@qti-components/base 2.1.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/abstract/qti-expression.d.ts +11 -0
- package/dist/abstract/qti-expression.js +19 -2
- package/dist/abstract/qti-expression.js.map +1 -1
- package/dist/abstract/qti-feedback.d.ts +1 -0
- package/dist/abstract/qti-feedback.js +44 -0
- package/dist/abstract/qti-feedback.js.map +1 -1
- package/dist/context/types/computed.types.d.ts +12 -0
- package/dist/context/types/computed.types.js.map +1 -1
- package/package.json +2 -2
|
@@ -2,6 +2,7 @@ import { LitElement } from 'lit';
|
|
|
2
2
|
import type { ItemContext } from '../context/types/item.types';
|
|
3
3
|
import type { QtiContext, QtiContextType } from '../context/qti.context';
|
|
4
4
|
import type { ResponseVariable, VariableDeclaration } from '../lib/variables';
|
|
5
|
+
import type { TestContext } from '../context/test.context';
|
|
5
6
|
export interface QtiExpressionBase<T> {
|
|
6
7
|
calculate(): Readonly<T>;
|
|
7
8
|
}
|
|
@@ -13,5 +14,15 @@ export declare abstract class QtiExpression<T> extends LitElement implements Qti
|
|
|
13
14
|
protected getResult(): Readonly<T>;
|
|
14
15
|
protected context?: ItemContext;
|
|
15
16
|
protected qtiContext?: QtiContext;
|
|
17
|
+
protected _testContext?: TestContext;
|
|
18
|
+
/**
|
|
19
|
+
* Resolve a declared variable from the closest available scope: item-level
|
|
20
|
+
* first, then the test-level outcome variables. So an expression used in an
|
|
21
|
+
* assessmentTest's outcome processing can reference a test-level outcome
|
|
22
|
+
* (e.g. a total set earlier in the same run) — which the QTI variable
|
|
23
|
+
* expression is specified to do. Returns null when unresolved rather than
|
|
24
|
+
* throwing, so callers degrade gracefully.
|
|
25
|
+
*/
|
|
26
|
+
protected resolveVariable(identifier: string): VariableDeclaration<string | string[] | null> | null;
|
|
16
27
|
getVariables: () => (ResponseVariable | VariableDeclaration<QtiContextType>)[];
|
|
17
28
|
}
|
|
@@ -9,6 +9,7 @@ import { css, html, LitElement } from 'lit';
|
|
|
9
9
|
import { state } from 'lit/decorators.js';
|
|
10
10
|
import { itemContext } from '../context/item.context';
|
|
11
11
|
import { qtiContext } from '../context/qti.context';
|
|
12
|
+
import { testContext } from '../context/test.context';
|
|
12
13
|
export class QtiExpression extends LitElement {
|
|
13
14
|
constructor() {
|
|
14
15
|
super(...arguments);
|
|
@@ -48,8 +49,7 @@ export class QtiExpression extends LitElement {
|
|
|
48
49
|
type: 'context'
|
|
49
50
|
};
|
|
50
51
|
}
|
|
51
|
-
|
|
52
|
-
return variable;
|
|
52
|
+
return this.resolveVariable(identifier);
|
|
53
53
|
}
|
|
54
54
|
case 'qti-multiple': {
|
|
55
55
|
const multiple = e;
|
|
@@ -156,6 +156,19 @@ export class QtiExpression extends LitElement {
|
|
|
156
156
|
getResult() {
|
|
157
157
|
throw new Error('Not implemented');
|
|
158
158
|
}
|
|
159
|
+
/**
|
|
160
|
+
* Resolve a declared variable from the closest available scope: item-level
|
|
161
|
+
* first, then the test-level outcome variables. So an expression used in an
|
|
162
|
+
* assessmentTest's outcome processing can reference a test-level outcome
|
|
163
|
+
* (e.g. a total set earlier in the same run) — which the QTI variable
|
|
164
|
+
* expression is specified to do. Returns null when unresolved rather than
|
|
165
|
+
* throwing, so callers degrade gracefully.
|
|
166
|
+
*/
|
|
167
|
+
resolveVariable(identifier) {
|
|
168
|
+
return (this.context?.variables.find(v => v.identifier === identifier) ??
|
|
169
|
+
this._testContext?.testOutcomeVariables?.find(v => v.identifier === identifier) ??
|
|
170
|
+
null);
|
|
171
|
+
}
|
|
159
172
|
}
|
|
160
173
|
__decorate([
|
|
161
174
|
state()
|
|
@@ -168,4 +181,8 @@ __decorate([
|
|
|
168
181
|
consume({ context: qtiContext, subscribe: true }),
|
|
169
182
|
state()
|
|
170
183
|
], QtiExpression.prototype, "qtiContext", void 0);
|
|
184
|
+
__decorate([
|
|
185
|
+
consume({ context: testContext, subscribe: true }),
|
|
186
|
+
state()
|
|
187
|
+
], QtiExpression.prototype, "_testContext", void 0);
|
|
171
188
|
//# sourceMappingURL=qti-expression.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"qti-expression.js","sourceRoot":"","sources":["../../src/abstract/qti-expression.ts"],"names":[],"mappings":"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","sourcesContent":["import { consume } from '@lit/context';\nimport { css, html, LitElement } from 'lit';\nimport { state } from 'lit/decorators.js';\n\nimport { itemContext } from '../context/item.context';\nimport { qtiContext } from '../context/qti.context';\n\nimport type { ItemContext } from '../context/types/item.types';\nimport type { QtiContext, QtiContextType } from '../context/qti.context';\nimport type { ResponseVariable, VariableDeclaration } from '../lib/variables';\n\nexport interface QtiExpressionBase<T> {\n // get assessmentItem(): QtiAssessmentItem;\n\n // getVariables(): VariableDeclaration<number | string | (number | string)[] | null>[];\n calculate(): Readonly<T>;\n}\n\nexport abstract class QtiExpression<T> extends LitElement implements QtiExpressionBase<T> {\n @state()\n protected result: any;\n\n /*\n * Written here rather than imported from a .css file, and that is a packaging constraint rather\n * than a style preference.\n *\n * The rule used to live in qti-expression.css, imported with Vite's inline-stylesheet query so\n * DevTools could show source-file provenance. That query is a VITE specifier, and tsc copies it\n * verbatim into dist — so the published package shipped an import only a Vite-based consumer\n * could resolve. Every other consumer broke on it: plain Node, tsx, any non-Vite bundler. It was\n * the single such specifier reachable from this package, and it took down QTI-Editor's schema\n * check, which loads the components under tsx to build the real ProseMirror schema\n * (ERR_UNKNOWN_FILE_EXTENSION on a file that has nothing to do with schemas).\n *\n * A `build:assets` step used to copy the .css into dist beside it. That went too — it existed\n * only to give the unresolvable import something to point at.\n *\n * Two lines of CSS are not worth a dist that only one bundler can consume. If this ever grows\n * into a real stylesheet, give it a `.styles.ts` exporting a `css` template — the house pattern\n * everywhere else in this repo — rather than reintroducing a bundler-specific import.\n *\n * The element renders a debug <pre> of its computed result plus a <slot>; hiding the slot is all\n * the styling it has ever had.\n */\n static override styles = css`\n slot {\n display: none;\n }\n `;\n\n override render() {\n return html`<pre>${JSON.stringify(this.result, null, 2)}</pre>\n <slot></slot>`;\n }\n\n public calculate(): Readonly<T> {\n this.result = this.getResult();\n return this.result;\n }\n\n protected getResult(): Readonly<T> {\n throw new Error('Not implemented');\n }\n\n @consume({ context: itemContext, subscribe: true })\n @state()\n protected context?: ItemContext;\n\n @consume({ context: qtiContext, subscribe: true })\n @state()\n protected qtiContext?: QtiContext;\n\n getVariables = (): (ResponseVariable | VariableDeclaration<QtiContextType>)[] =>\n // FIXME: if this itself is multiple, this will never enter the qti-multiple switch\n // See this example here: https://github.com/1EdTech/qti-examples/blob/master/qtiv3-examples/packaging/items/Example05-feedbackBlock-adaptive.xml\n\n Array.from(this.children)\n .map((e: Element) => {\n console.debug('getVariables', e.tagName.toLowerCase());\n switch (e.tagName.toLowerCase()) {\n case 'qti-base-value': {\n return {\n baseType: e.getAttribute('base-type'),\n value: e.textContent.trim(),\n cardinality: 'single'\n } as ResponseVariable;\n }\n case 'qti-variable': {\n const identifier = e.getAttribute('identifier') || '';\n\n // Special handling for QTI_CONTEXT\n if (identifier === 'QTI_CONTEXT') {\n if (!this.qtiContext?.QTI_CONTEXT) {\n console.warn('QTI_CONTEXT not available');\n return {\n identifier: 'QTI_CONTEXT',\n baseType: 'record',\n value: this.qtiContext.QTI_CONTEXT,\n cardinality: 'record',\n type: 'context'\n } as VariableDeclaration<QtiContextType>;\n }\n return {\n identifier: 'QTI_CONTEXT',\n baseType: 'record',\n value: this.qtiContext.QTI_CONTEXT,\n cardinality: 'record',\n type: 'context'\n } as VariableDeclaration<QtiContextType>;\n }\n\n const variable = this.context.variables.find(v => v.identifier === identifier) || null;\n return variable;\n }\n case 'qti-multiple': {\n const multiple = e as QtiExpression<ResponseVariable[]>;\n\n const values = multiple.getResult();\n console.debug('values', values);\n if (values?.length > 0) {\n return {\n identifier: '',\n baseType: values[0].baseType,\n value: values.map(v => v.value),\n cardinality: 'multiple',\n type: 'response'\n } as ResponseVariable;\n }\n return null;\n }\n case 'qti-ordered': {\n const multiple = e as QtiExpression<ResponseVariable[]>;\n const values = multiple.getResult();\n if (values?.length > 0) {\n return {\n identifier: '',\n baseType: values[0].baseType,\n value: values.map(v => v.value),\n cardinality: 'ordered',\n type: 'response'\n } as ResponseVariable;\n }\n return null;\n }\n case 'qti-repeat': {\n const repeat = e as QtiExpression<ResponseVariable[]>;\n const values = repeat.getResult();\n if (values?.length > 0) {\n return values;\n }\n return null;\n }\n case 'qti-correct': {\n const identifier = e.getAttribute('identifier') || '';\n const responseVariable: ResponseVariable =\n this.context?.variables?.find(v => v.identifier === identifier) || null;\n return {\n baseType: responseVariable.baseType,\n value: responseVariable.correctResponse,\n cardinality: responseVariable.cardinality\n } as ResponseVariable;\n }\n default: {\n // added for use of qti-equal-rounded\n try {\n const expression = e as QtiExpression<number>;\n const value = expression.getResult();\n return {\n baseType: 'integer',\n value: value?.toString() || null,\n cardinality: 'single'\n } as ResponseVariable;\n } catch (error) {\n console.warn('default not sufficient');\n }\n return null;\n }\n }\n })\n .flatMap(v => (Array.isArray(v) ? v : [v]))\n .filter(v => v !== null);\n}\n"]}
|
|
1
|
+
{"version":3,"file":"qti-expression.js","sourceRoot":"","sources":["../../src/abstract/qti-expression.ts"],"names":[],"mappings":"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","sourcesContent":["import { consume } from '@lit/context';\nimport { css, html, LitElement } from 'lit';\nimport { state } from 'lit/decorators.js';\n\nimport { itemContext } from '../context/item.context';\nimport { qtiContext } from '../context/qti.context';\nimport { testContext } from '../context/test.context';\n\nimport type { ItemContext } from '../context/types/item.types';\nimport type { QtiContext, QtiContextType } from '../context/qti.context';\nimport type { ResponseVariable, VariableDeclaration } from '../lib/variables';\nimport type { TestContext } from '../context/test.context';\n\nexport interface QtiExpressionBase<T> {\n // get assessmentItem(): QtiAssessmentItem;\n\n // getVariables(): VariableDeclaration<number | string | (number | string)[] | null>[];\n calculate(): Readonly<T>;\n}\n\nexport abstract class QtiExpression<T> extends LitElement implements QtiExpressionBase<T> {\n @state()\n protected result: any;\n\n /*\n * Written here rather than imported from a .css file, and that is a packaging constraint rather\n * than a style preference.\n *\n * The rule used to live in qti-expression.css, imported with Vite's inline-stylesheet query so\n * DevTools could show source-file provenance. That query is a VITE specifier, and tsc copies it\n * verbatim into dist — so the published package shipped an import only a Vite-based consumer\n * could resolve. Every other consumer broke on it: plain Node, tsx, any non-Vite bundler. It was\n * the single such specifier reachable from this package, and it took down QTI-Editor's schema\n * check, which loads the components under tsx to build the real ProseMirror schema\n * (ERR_UNKNOWN_FILE_EXTENSION on a file that has nothing to do with schemas).\n *\n * A `build:assets` step used to copy the .css into dist beside it. That went too — it existed\n * only to give the unresolvable import something to point at.\n *\n * Two lines of CSS are not worth a dist that only one bundler can consume. If this ever grows\n * into a real stylesheet, give it a `.styles.ts` exporting a `css` template — the house pattern\n * everywhere else in this repo — rather than reintroducing a bundler-specific import.\n *\n * The element renders a debug <pre> of its computed result plus a <slot>; hiding the slot is all\n * the styling it has ever had.\n */\n static override styles = css`\n slot {\n display: none;\n }\n `;\n\n override render() {\n return html`<pre>${JSON.stringify(this.result, null, 2)}</pre>\n <slot></slot>`;\n }\n\n public calculate(): Readonly<T> {\n this.result = this.getResult();\n return this.result;\n }\n\n protected getResult(): Readonly<T> {\n throw new Error('Not implemented');\n }\n\n @consume({ context: itemContext, subscribe: true })\n @state()\n protected context?: ItemContext;\n\n @consume({ context: qtiContext, subscribe: true })\n @state()\n protected qtiContext?: QtiContext;\n\n @consume({ context: testContext, subscribe: true })\n @state()\n protected _testContext?: TestContext;\n\n /**\n * Resolve a declared variable from the closest available scope: item-level\n * first, then the test-level outcome variables. So an expression used in an\n * assessmentTest's outcome processing can reference a test-level outcome\n * (e.g. a total set earlier in the same run) — which the QTI variable\n * expression is specified to do. Returns null when unresolved rather than\n * throwing, so callers degrade gracefully.\n */\n protected resolveVariable(identifier: string): VariableDeclaration<string | string[] | null> | null {\n return (\n this.context?.variables.find(v => v.identifier === identifier) ??\n this._testContext?.testOutcomeVariables?.find(v => v.identifier === identifier) ??\n null\n );\n }\n\n getVariables = (): (ResponseVariable | VariableDeclaration<QtiContextType>)[] =>\n // FIXME: if this itself is multiple, this will never enter the qti-multiple switch\n // See this example here: https://github.com/1EdTech/qti-examples/blob/master/qtiv3-examples/packaging/items/Example05-feedbackBlock-adaptive.xml\n\n Array.from(this.children)\n .map((e: Element) => {\n console.debug('getVariables', e.tagName.toLowerCase());\n switch (e.tagName.toLowerCase()) {\n case 'qti-base-value': {\n return {\n baseType: e.getAttribute('base-type'),\n value: e.textContent.trim(),\n cardinality: 'single'\n } as ResponseVariable;\n }\n case 'qti-variable': {\n const identifier = e.getAttribute('identifier') || '';\n\n // Special handling for QTI_CONTEXT\n if (identifier === 'QTI_CONTEXT') {\n if (!this.qtiContext?.QTI_CONTEXT) {\n console.warn('QTI_CONTEXT not available');\n return {\n identifier: 'QTI_CONTEXT',\n baseType: 'record',\n value: this.qtiContext.QTI_CONTEXT,\n cardinality: 'record',\n type: 'context'\n } as VariableDeclaration<QtiContextType>;\n }\n return {\n identifier: 'QTI_CONTEXT',\n baseType: 'record',\n value: this.qtiContext.QTI_CONTEXT,\n cardinality: 'record',\n type: 'context'\n } as VariableDeclaration<QtiContextType>;\n }\n\n return this.resolveVariable(identifier);\n }\n case 'qti-multiple': {\n const multiple = e as QtiExpression<ResponseVariable[]>;\n\n const values = multiple.getResult();\n console.debug('values', values);\n if (values?.length > 0) {\n return {\n identifier: '',\n baseType: values[0].baseType,\n value: values.map(v => v.value),\n cardinality: 'multiple',\n type: 'response'\n } as ResponseVariable;\n }\n return null;\n }\n case 'qti-ordered': {\n const multiple = e as QtiExpression<ResponseVariable[]>;\n const values = multiple.getResult();\n if (values?.length > 0) {\n return {\n identifier: '',\n baseType: values[0].baseType,\n value: values.map(v => v.value),\n cardinality: 'ordered',\n type: 'response'\n } as ResponseVariable;\n }\n return null;\n }\n case 'qti-repeat': {\n const repeat = e as QtiExpression<ResponseVariable[]>;\n const values = repeat.getResult();\n if (values?.length > 0) {\n return values;\n }\n return null;\n }\n case 'qti-correct': {\n const identifier = e.getAttribute('identifier') || '';\n const responseVariable: ResponseVariable =\n this.context?.variables?.find(v => v.identifier === identifier) || null;\n return {\n baseType: responseVariable.baseType,\n value: responseVariable.correctResponse,\n cardinality: responseVariable.cardinality\n } as ResponseVariable;\n }\n default: {\n // added for use of qti-equal-rounded\n try {\n const expression = e as QtiExpression<number>;\n const value = expression.getResult();\n return {\n baseType: 'integer',\n value: value?.toString() || null,\n cardinality: 'single'\n } as ResponseVariable;\n } catch (error) {\n console.warn('default not sufficient');\n }\n return null;\n }\n }\n })\n .flatMap(v => (Array.isArray(v) ? v : [v]))\n .filter(v => v !== null);\n}\n"]}
|
|
@@ -7,6 +7,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
7
7
|
import { consume } from '@lit/context';
|
|
8
8
|
import { LitElement } from 'lit';
|
|
9
9
|
import { property, state } from 'lit/decorators.js';
|
|
10
|
+
import { computedContext } from '../context/computed.context';
|
|
10
11
|
import { itemContext } from '../context/item.context';
|
|
11
12
|
import { IsNullOrUndefined } from '../utils/utils';
|
|
12
13
|
export class QtiFeedback extends LitElement {
|
|
@@ -40,8 +41,47 @@ export class QtiFeedback extends LitElement {
|
|
|
40
41
|
this.#showFeedback(isFound);
|
|
41
42
|
}
|
|
42
43
|
#showFeedback(value) {
|
|
44
|
+
if (!this.#sessionControlAllowsFeedback()) {
|
|
45
|
+
this.showStatus = 'off';
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
43
48
|
this.showStatus = (value && this.showHide === 'show') || (!value && this.showHide === 'hide') ? 'on' : 'off';
|
|
44
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* The QTI `show-feedback` constraint governs exactly one state: after the end of
|
|
52
|
+
* the last attempt. Up to that point the spec requires any applicable feedback to
|
|
53
|
+
* be shown — "a value of max-attempts greater than 1, by definition, indicates
|
|
54
|
+
* that any applicable feedback must be shown" — and only "once the maximum number
|
|
55
|
+
* of allowed attempts have been used (or for adaptive items, completionStatus has
|
|
56
|
+
* been set to completed)" does `show-feedback` decide.
|
|
57
|
+
*
|
|
58
|
+
* This only ever forces feedback off, so `show-feedback` being true settles it:
|
|
59
|
+
* there is no state in which the constraint hides feedback. It defaults to
|
|
60
|
+
* false, and an absent value suppresses once the attempts are gone.
|
|
61
|
+
*
|
|
62
|
+
* That leaves one question — is the item out of attempts? — and the two item
|
|
63
|
+
* kinds answer it from different variables, so each reads only its own.
|
|
64
|
+
*/
|
|
65
|
+
#sessionControlAllowsFeedback() {
|
|
66
|
+
if (!this._computedContext)
|
|
67
|
+
return true;
|
|
68
|
+
const activeItem = this._computedContext.testParts
|
|
69
|
+
?.find(tp => tp.active)
|
|
70
|
+
?.sections.flatMap(s => s.items)
|
|
71
|
+
.find(i => i.active);
|
|
72
|
+
if (!activeItem || activeItem.showFeedback)
|
|
73
|
+
return true;
|
|
74
|
+
// Adaptive items ignore max-attempts: "the number of attempts is limited by
|
|
75
|
+
// the value of the completionStatus built-in outcome variable".
|
|
76
|
+
if (activeItem.adaptive)
|
|
77
|
+
return activeItem.completionStatus !== 'completed';
|
|
78
|
+
// `max-attempts="0"` is "no limit", so the last attempt never arrives.
|
|
79
|
+
const maxAttempts = activeItem.maxAttempts ?? 1;
|
|
80
|
+
if (maxAttempts <= 0)
|
|
81
|
+
return true;
|
|
82
|
+
const numAttempts = Number(this._context?.variables?.find(v => v.identifier === 'numAttempts')?.value) || 0;
|
|
83
|
+
return numAttempts < maxAttempts;
|
|
84
|
+
}
|
|
45
85
|
}
|
|
46
86
|
__decorate([
|
|
47
87
|
property({ type: String, attribute: 'show-hide' })
|
|
@@ -59,4 +99,8 @@ __decorate([
|
|
|
59
99
|
consume({ context: itemContext, subscribe: true }),
|
|
60
100
|
state()
|
|
61
101
|
], QtiFeedback.prototype, "_context", void 0);
|
|
102
|
+
__decorate([
|
|
103
|
+
consume({ context: computedContext, subscribe: true }),
|
|
104
|
+
state()
|
|
105
|
+
], QtiFeedback.prototype, "_computedContext", void 0);
|
|
62
106
|
//# sourceMappingURL=qti-feedback.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"qti-feedback.js","sourceRoot":"","sources":["../../src/abstract/qti-feedback.ts"],"names":[],"mappings":";;;;;;AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,OAAO,EAAE,UAAU,EAAE,MAAM,KAAK,CAAC;AACjC,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAEpD,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AACtD,OAAO,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"qti-feedback.js","sourceRoot":"","sources":["../../src/abstract/qti-feedback.ts"],"names":[],"mappings":"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","sourcesContent":["import { consume } from '@lit/context';\nimport { LitElement } from 'lit';\nimport { property, state } from 'lit/decorators.js';\n\nimport { computedContext } from '../context/computed.context';\nimport { itemContext } from '../context/item.context';\nimport { IsNullOrUndefined } from '../utils/utils';\n\nimport type { ComputedContext } from '../context/types/computed.types';\nimport type { ItemContext } from '../context/types/item.types';\n\nexport abstract class QtiFeedback extends LitElement {\n @property({ type: String, attribute: 'show-hide' })\n protected showHide: 'show' | 'hide' = 'show';\n\n @property({ type: String, attribute: 'outcome-identifier' })\n public outcomeIdentifier: string;\n\n @property({ type: String })\n protected identifier: string;\n\n @property({ type: String, attribute: false })\n public showStatus: string;\n\n @consume({ context: itemContext, subscribe: true })\n @state()\n private _context?: ItemContext;\n\n @consume({ context: computedContext, subscribe: true })\n @state()\n private _computedContext?: ComputedContext;\n\n public override connectedCallback() {\n super.connectedCallback();\n this.dispatchEvent(\n new CustomEvent<QtiFeedback>('qti-register-feedback', {\n bubbles: true,\n composed: true,\n detail: this\n })\n );\n }\n\n public checkShowFeedback(outcomeIdentifier: string) {\n const outcomeVariable = this._context.variables.find(v => v.identifier === outcomeIdentifier) || null;\n if (this.outcomeIdentifier !== outcomeIdentifier || !outcomeVariable) return;\n let isFound = false;\n if (Array.isArray(outcomeVariable.value)) {\n isFound = outcomeVariable.value.includes(this.identifier);\n } else {\n isFound =\n (!IsNullOrUndefined(this.identifier) &&\n !IsNullOrUndefined(outcomeVariable?.value) &&\n this.identifier === outcomeVariable.value) ||\n false;\n }\n\n this.#showFeedback(isFound);\n }\n\n #showFeedback(value: boolean) {\n if (!this.#sessionControlAllowsFeedback()) {\n this.showStatus = 'off';\n return;\n }\n this.showStatus = (value && this.showHide === 'show') || (!value && this.showHide === 'hide') ? 'on' : 'off';\n }\n\n /**\n * The QTI `show-feedback` constraint governs exactly one state: after the end of\n * the last attempt. Up to that point the spec requires any applicable feedback to\n * be shown — \"a value of max-attempts greater than 1, by definition, indicates\n * that any applicable feedback must be shown\" — and only \"once the maximum number\n * of allowed attempts have been used (or for adaptive items, completionStatus has\n * been set to completed)\" does `show-feedback` decide.\n *\n * This only ever forces feedback off, so `show-feedback` being true settles it:\n * there is no state in which the constraint hides feedback. It defaults to\n * false, and an absent value suppresses once the attempts are gone.\n *\n * That leaves one question — is the item out of attempts? — and the two item\n * kinds answer it from different variables, so each reads only its own.\n */\n #sessionControlAllowsFeedback(): boolean {\n if (!this._computedContext) return true;\n const activeItem = this._computedContext.testParts\n ?.find(tp => tp.active)\n ?.sections.flatMap(s => s.items)\n .find(i => i.active);\n if (!activeItem || activeItem.showFeedback) return true;\n\n // Adaptive items ignore max-attempts: \"the number of attempts is limited by\n // the value of the completionStatus built-in outcome variable\".\n if (activeItem.adaptive) return activeItem.completionStatus !== 'completed';\n\n // `max-attempts=\"0\"` is \"no limit\", so the last attempt never arrives.\n const maxAttempts = activeItem.maxAttempts ?? 1;\n if (maxAttempts <= 0) return true;\n\n const numAttempts = Number(this._context?.variables?.find(v => v.identifier === 'numAttempts')?.value) || 0;\n return numAttempts < maxAttempts;\n }\n}\n"]}
|
|
@@ -5,6 +5,16 @@ export type ComputedItem = ComputedItemContext & {
|
|
|
5
5
|
type?: 'info' | 'regular';
|
|
6
6
|
index?: number;
|
|
7
7
|
active?: boolean;
|
|
8
|
+
allowSkipping?: boolean;
|
|
9
|
+
maxAttempts?: number;
|
|
10
|
+
showFeedback?: boolean;
|
|
11
|
+
showSolution?: boolean;
|
|
12
|
+
numAttempts?: number;
|
|
13
|
+
valid?: boolean;
|
|
14
|
+
isDefaultResponse?: boolean;
|
|
15
|
+
done?: boolean;
|
|
16
|
+
/** Last *ended attempt* reached the optimal outcome (best achievable score / correct response). */
|
|
17
|
+
optimal?: boolean;
|
|
8
18
|
};
|
|
9
19
|
export type ComputedContext = {
|
|
10
20
|
view: View;
|
|
@@ -15,6 +25,7 @@ export type ComputedContext = {
|
|
|
15
25
|
identifier: string;
|
|
16
26
|
navigationMode: 'linear' | 'nonlinear';
|
|
17
27
|
submissionMode: 'individual' | 'simultaneous';
|
|
28
|
+
allowSkipping?: boolean;
|
|
18
29
|
sections: {
|
|
19
30
|
active?: boolean;
|
|
20
31
|
identifier: string;
|
|
@@ -23,6 +34,7 @@ export type ComputedContext = {
|
|
|
23
34
|
items: ComputedItem[];
|
|
24
35
|
navigationMode: 'linear' | 'nonlinear';
|
|
25
36
|
submissionMode: 'individual' | 'simultaneous';
|
|
37
|
+
allowSkipping?: boolean;
|
|
26
38
|
}[];
|
|
27
39
|
}[];
|
|
28
40
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"computed.types.js","sourceRoot":"","sources":["../../../src/context/types/computed.types.ts"],"names":[],"mappings":"","sourcesContent":["import type { View } from '../session.context';\nimport type { ComputedItemContext } from './computed-item.types';\n\nexport type ComputedItem = ComputedItemContext & {\n categories?: string[]; // not necessary for outside world\n type?: 'info' | 'regular';\n index?: number;\n active?: boolean;\n};\n\nexport type ComputedContext = {\n view: View;\n identifier: string;\n title: string;\n testParts: {\n active?: boolean;\n identifier: string;\n navigationMode: 'linear' | 'nonlinear';\n submissionMode: 'individual' | 'simultaneous';\n sections: {\n active?: boolean;\n identifier: string;\n title: string;\n completed?: boolean;\n items: ComputedItem[];\n navigationMode: 'linear' | 'nonlinear';\n submissionMode: 'individual' | 'simultaneous';\n }[];\n }[];\n};\n"]}
|
|
1
|
+
{"version":3,"file":"computed.types.js","sourceRoot":"","sources":["../../../src/context/types/computed.types.ts"],"names":[],"mappings":"","sourcesContent":["import type { View } from '../session.context';\nimport type { ComputedItemContext } from './computed-item.types';\n\nexport type ComputedItem = ComputedItemContext & {\n categories?: string[]; // not necessary for outside world\n type?: 'info' | 'regular';\n index?: number;\n active?: boolean;\n allowSkipping?: boolean;\n maxAttempts?: number;\n showFeedback?: boolean;\n showSolution?: boolean;\n numAttempts?: number;\n valid?: boolean;\n isDefaultResponse?: boolean;\n done?: boolean;\n /** Last *ended attempt* reached the optimal outcome (best achievable score / correct response). */\n optimal?: boolean;\n};\n\nexport type ComputedContext = {\n view: View;\n identifier: string;\n title: string;\n testParts: {\n active?: boolean;\n identifier: string;\n navigationMode: 'linear' | 'nonlinear';\n submissionMode: 'individual' | 'simultaneous';\n allowSkipping?: boolean;\n sections: {\n active?: boolean;\n identifier: string;\n title: string;\n completed?: boolean;\n items: ComputedItem[];\n navigationMode: 'linear' | 'nonlinear';\n submissionMode: 'individual' | 'simultaneous';\n allowSkipping?: boolean;\n }[];\n }[];\n};\n"]}
|
package/package.json
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qti-components/base",
|
|
3
3
|
"description": "QTI shared utilities",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.2.0",
|
|
5
5
|
"author": "",
|
|
6
6
|
"dependencies": {
|
|
7
7
|
"@qti-components/utilities": "^1.3.3"
|
|
8
8
|
},
|
|
9
9
|
"devDependencies": {
|
|
10
10
|
"@lit/context": "^1.1.6",
|
|
11
|
-
"lit": "^3.3.
|
|
11
|
+
"lit": "^3.3.3"
|
|
12
12
|
},
|
|
13
13
|
"keywords": [
|
|
14
14
|
"qti",
|