@qti-components/base 2.0.1 → 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.
@@ -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
- const variable = this.context.variables.find(v => v.identifier === identifier) || null;
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":";;;;;;AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,KAAK,CAAC;AAC5C,OAAO,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAE1C,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AACtD,OAAO,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAapD,MAAM,OAAgB,aAAiB,SAAQ,UAAU;IAAzD;;QAsDE,iBAAY,GAAG,GAA+D,EAAE;QAC9E,mFAAmF;QACnF,iJAAiJ;QAEjJ,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC;aACtB,GAAG,CAAC,CAAC,CAAU,EAAE,EAAE;YAClB,OAAO,CAAC,KAAK,CAAC,cAAc,EAAE,CAAC,CAAC,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC;YACvD,QAAQ,CAAC,CAAC,OAAO,CAAC,WAAW,EAAE,EAAE,CAAC;gBAChC,KAAK,gBAAgB,CAAC,CAAC,CAAC;oBACtB,OAAO;wBACL,QAAQ,EAAE,CAAC,CAAC,YAAY,CAAC,WAAW,CAAC;wBACrC,KAAK,EAAE,CAAC,CAAC,WAAW,CAAC,IAAI,EAAE;wBAC3B,WAAW,EAAE,QAAQ;qBACF,CAAC;gBACxB,CAAC;gBACD,KAAK,cAAc,CAAC,CAAC,CAAC;oBACpB,MAAM,UAAU,GAAG,CAAC,CAAC,YAAY,CAAC,YAAY,CAAC,IAAI,EAAE,CAAC;oBAEtD,mCAAmC;oBACnC,IAAI,UAAU,KAAK,aAAa,EAAE,CAAC;wBACjC,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,WAAW,EAAE,CAAC;4BAClC,OAAO,CAAC,IAAI,CAAC,2BAA2B,CAAC,CAAC;4BAC1C,OAAO;gCACL,UAAU,EAAE,aAAa;gCACzB,QAAQ,EAAE,QAAQ;gCAClB,KAAK,EAAE,IAAI,CAAC,UAAU,CAAC,WAAW;gCAClC,WAAW,EAAE,QAAQ;gCACrB,IAAI,EAAE,SAAS;6BACuB,CAAC;wBAC3C,CAAC;wBACD,OAAO;4BACL,UAAU,EAAE,aAAa;4BACzB,QAAQ,EAAE,QAAQ;4BAClB,KAAK,EAAE,IAAI,CAAC,UAAU,CAAC,WAAW;4BAClC,WAAW,EAAE,QAAQ;4BACrB,IAAI,EAAE,SAAS;yBACuB,CAAC;oBAC3C,CAAC;oBAED,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,UAAU,KAAK,UAAU,CAAC,IAAI,IAAI,CAAC;oBACvF,OAAO,QAAQ,CAAC;gBAClB,CAAC;gBACD,KAAK,cAAc,CAAC,CAAC,CAAC;oBACpB,MAAM,QAAQ,GAAG,CAAsC,CAAC;oBAExD,MAAM,MAAM,GAAG,QAAQ,CAAC,SAAS,EAAE,CAAC;oBACpC,OAAO,CAAC,KAAK,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;oBAChC,IAAI,MAAM,EAAE,MAAM,GAAG,CAAC,EAAE,CAAC;wBACvB,OAAO;4BACL,UAAU,EAAE,EAAE;4BACd,QAAQ,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,QAAQ;4BAC5B,KAAK,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC;4BAC/B,WAAW,EAAE,UAAU;4BACvB,IAAI,EAAE,UAAU;yBACG,CAAC;oBACxB,CAAC;oBACD,OAAO,IAAI,CAAC;gBACd,CAAC;gBACD,KAAK,aAAa,CAAC,CAAC,CAAC;oBACnB,MAAM,QAAQ,GAAG,CAAsC,CAAC;oBACxD,MAAM,MAAM,GAAG,QAAQ,CAAC,SAAS,EAAE,CAAC;oBACpC,IAAI,MAAM,EAAE,MAAM,GAAG,CAAC,EAAE,CAAC;wBACvB,OAAO;4BACL,UAAU,EAAE,EAAE;4BACd,QAAQ,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,QAAQ;4BAC5B,KAAK,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC;4BAC/B,WAAW,EAAE,SAAS;4BACtB,IAAI,EAAE,UAAU;yBACG,CAAC;oBACxB,CAAC;oBACD,OAAO,IAAI,CAAC;gBACd,CAAC;gBACD,KAAK,YAAY,CAAC,CAAC,CAAC;oBAClB,MAAM,MAAM,GAAG,CAAsC,CAAC;oBACtD,MAAM,MAAM,GAAG,MAAM,CAAC,SAAS,EAAE,CAAC;oBAClC,IAAI,MAAM,EAAE,MAAM,GAAG,CAAC,EAAE,CAAC;wBACvB,OAAO,MAAM,CAAC;oBAChB,CAAC;oBACD,OAAO,IAAI,CAAC;gBACd,CAAC;gBACD,KAAK,aAAa,CAAC,CAAC,CAAC;oBACnB,MAAM,UAAU,GAAG,CAAC,CAAC,YAAY,CAAC,YAAY,CAAC,IAAI,EAAE,CAAC;oBACtD,MAAM,gBAAgB,GACpB,IAAI,CAAC,OAAO,EAAE,SAAS,EAAE,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,UAAU,KAAK,UAAU,CAAC,IAAI,IAAI,CAAC;oBAC1E,OAAO;wBACL,QAAQ,EAAE,gBAAgB,CAAC,QAAQ;wBACnC,KAAK,EAAE,gBAAgB,CAAC,eAAe;wBACvC,WAAW,EAAE,gBAAgB,CAAC,WAAW;qBACtB,CAAC;gBACxB,CAAC;gBACD,OAAO,CAAC,CAAC,CAAC;oBACR,qCAAqC;oBACrC,IAAI,CAAC;wBACH,MAAM,UAAU,GAAG,CAA0B,CAAC;wBAC9C,MAAM,KAAK,GAAG,UAAU,CAAC,SAAS,EAAE,CAAC;wBACrC,OAAO;4BACL,QAAQ,EAAE,SAAS;4BACnB,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,IAAI;4BAChC,WAAW,EAAE,QAAQ;yBACF,CAAC;oBACxB,CAAC;oBAAC,OAAO,KAAK,EAAE,CAAC;wBACf,OAAO,CAAC,IAAI,CAAC,wBAAwB,CAAC,CAAC;oBACzC,CAAC;oBACD,OAAO,IAAI,CAAC;gBACd,CAAC;YACH,CAAC;QACH,CAAC,CAAC;aACD,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;aAC1C,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC;IAC/B,CAAC;IA/JC;;;;;;;;;;;;;;;;;;;;;OAqBG;aACa,WAAM,GAAG,GAAG,CAAA;;;;GAI3B,AAJqB,CAIpB;IAEO,MAAM;QACb,OAAO,IAAI,CAAA,QAAQ,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;oBACvC,CAAC;IACnB,CAAC;IAEM,SAAS;QACd,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,SAAS,EAAE,CAAC;QAC/B,OAAO,IAAI,CAAC,MAAM,CAAC;IACrB,CAAC;IAES,SAAS;QACjB,MAAM,IAAI,KAAK,CAAC,iBAAiB,CAAC,CAAC;IACrC,CAAC;;AA1CS;IADT,KAAK,EAAE;6CACc;AA8CZ;IAFT,OAAO,CAAC,EAAE,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC;IAClD,KAAK,EAAE;8CACwB;AAItB;IAFT,OAAO,CAAC,EAAE,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC;IACjD,KAAK,EAAE;iDAC0B","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"]}
@@ -6,6 +6,7 @@ export declare abstract class QtiFeedback extends LitElement {
6
6
  protected identifier: string;
7
7
  showStatus: string;
8
8
  private _context?;
9
+ private _computedContext?;
9
10
  connectedCallback(): void;
10
11
  checkShowFeedback(outcomeIdentifier: string): void;
11
12
  }
@@ -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;AAInD,MAAM,OAAgB,WAAY,SAAQ,UAAU;IAApD;;QAEY,aAAQ,GAAoB,MAAM,CAAC;IA8C/C,CAAC;IA/BiB,iBAAiB;QAC/B,KAAK,CAAC,iBAAiB,EAAE,CAAC;QAC1B,IAAI,CAAC,aAAa,CAChB,IAAI,WAAW,CAAc,uBAAuB,EAAE;YACpD,OAAO,EAAE,IAAI;YACb,QAAQ,EAAE,IAAI;YACd,MAAM,EAAE,IAAI;SACb,CAAC,CACH,CAAC;IACJ,CAAC;IAEM,iBAAiB,CAAC,iBAAyB;QAChD,MAAM,eAAe,GAAG,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,UAAU,KAAK,iBAAiB,CAAC,IAAI,IAAI,CAAC;QACtG,IAAI,IAAI,CAAC,iBAAiB,KAAK,iBAAiB,IAAI,CAAC,eAAe;YAAE,OAAO;QAC7E,IAAI,OAAO,GAAG,KAAK,CAAC;QACpB,IAAI,KAAK,CAAC,OAAO,CAAC,eAAe,CAAC,KAAK,CAAC,EAAE,CAAC;YACzC,OAAO,GAAG,eAAe,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QAC5D,CAAC;aAAM,CAAC;YACN,OAAO;gBACL,CAAC,CAAC,iBAAiB,CAAC,IAAI,CAAC,UAAU,CAAC;oBAClC,CAAC,iBAAiB,CAAC,eAAe,EAAE,KAAK,CAAC;oBAC1C,IAAI,CAAC,UAAU,KAAK,eAAe,CAAC,KAAK,CAAC;oBAC5C,KAAK,CAAC;QACV,CAAC;QAED,IAAI,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC;IAC9B,CAAC;IAED,aAAa,CAAC,KAAc;QAC1B,IAAI,CAAC,UAAU,GAAG,CAAC,KAAK,IAAI,IAAI,CAAC,QAAQ,KAAK,MAAM,CAAC,IAAI,CAAC,CAAC,KAAK,IAAI,IAAI,CAAC,QAAQ,KAAK,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC;IAC/G,CAAC;CACF;AA9CW;IADT,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,WAAW,EAAE,CAAC;6CACN;AAGtC;IADN,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,oBAAoB,EAAE,CAAC;sDAC3B;AAGvB;IADT,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;+CACE;AAGtB;IADN,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC;+CACnB;AAIlB;IAFP,OAAO,CAAC,EAAE,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC;IAClD,KAAK,EAAE;6CACuB","sourcesContent":["import { consume } from '@lit/context';\nimport { LitElement } from 'lit';\nimport { property, state } from 'lit/decorators.js';\n\nimport { itemContext } from '../context/item.context';\nimport { IsNullOrUndefined } from '../utils/utils';\n\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 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 this.showStatus = (value && this.showHide === 'show') || (!value && this.showHide === 'hide') ? 'on' : 'off';\n }\n}\n"]}
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/dist/index.d.ts CHANGED
@@ -23,5 +23,6 @@ export * from './lib/expression-result';
23
23
  export * from './lib/interaction.interface';
24
24
  export * from './lib/variables';
25
25
  export * from './lib/qti-rule-base';
26
+ export * from './utils/custom-state-set';
26
27
  export * from './utils/scoring-helper';
27
28
  export * from './utils/utils';
package/dist/index.js CHANGED
@@ -23,6 +23,7 @@ export * from './lib/expression-result';
23
23
  export * from './lib/interaction.interface';
24
24
  export * from './lib/variables';
25
25
  export * from './lib/qti-rule-base';
26
+ export * from './utils/custom-state-set';
26
27
  export * from './utils/scoring-helper';
27
28
  export * from './utils/utils';
28
29
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,YAAY,CAAC;AAEpB,cAAc,iCAAiC,CAAC;AAChD,cAAc,qCAAqC,CAAC;AACpD,cAAc,4BAA4B,CAAC;AAC3C,cAAc,gCAAgC,CAAC;AAC/C,cAAc,0BAA0B,CAAC;AACzC,cAAc,+BAA+B,CAAC;AAC9C,cAAc,wBAAwB,CAAC;AACvC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,uBAAuB,CAAC;AACtC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,wBAAwB,CAAC;AAEvC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,oCAAoC,CAAC;AAEnD,cAAc,wBAAwB,CAAC;AACvC,cAAc,qCAAqC,CAAC;AACpD,cAAc,qCAAqC,CAAC;AACpD,cAAc,yBAAyB,CAAC;AACxC,cAAc,2BAA2B,CAAC;AAE1C,cAAc,gBAAgB,CAAC;AAC/B,cAAc,yBAAyB,CAAC;AACxC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,iBAAiB,CAAC;AAChC,cAAc,qBAAqB,CAAC;AAEpC,cAAc,wBAAwB,CAAC;AACvC,cAAc,eAAe,CAAC","sourcesContent":["import './register';\n\nexport * from './context/computed-item.context';\nexport * from './context/types/computed-item.types';\nexport * from './context/computed.context';\nexport * from './context/types/computed.types';\nexport * from './context/config.context';\nexport * from './context/interaction.context';\nexport * from './context/item.context';\nexport * from './context/types/item.types';\nexport * from './context/qti.context';\nexport * from './context/session.context';\nexport * from './context/test.context';\n\nexport * from './styles/box-sizing.styles';\nexport * from './styles/drop-region.styles';\nexport * from './styles/validation-message.styles';\n\nexport * from './abstract/interaction';\nexport * from './abstract/qti-variable-declaration';\nexport * from './abstract/qti-condition-expression';\nexport * from './abstract/qti-feedback';\nexport * from './abstract/qti-expression';\n\nexport * from './lib/response';\nexport * from './lib/expression-result';\nexport * from './lib/interaction.interface';\nexport * from './lib/variables';\nexport * from './lib/qti-rule-base';\n\nexport * from './utils/scoring-helper';\nexport * from './utils/utils';\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,YAAY,CAAC;AAEpB,cAAc,iCAAiC,CAAC;AAChD,cAAc,qCAAqC,CAAC;AACpD,cAAc,4BAA4B,CAAC;AAC3C,cAAc,gCAAgC,CAAC;AAC/C,cAAc,0BAA0B,CAAC;AACzC,cAAc,+BAA+B,CAAC;AAC9C,cAAc,wBAAwB,CAAC;AACvC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,uBAAuB,CAAC;AACtC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,wBAAwB,CAAC;AAEvC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,oCAAoC,CAAC;AAEnD,cAAc,wBAAwB,CAAC;AACvC,cAAc,qCAAqC,CAAC;AACpD,cAAc,qCAAqC,CAAC;AACpD,cAAc,yBAAyB,CAAC;AACxC,cAAc,2BAA2B,CAAC;AAE1C,cAAc,gBAAgB,CAAC;AAC/B,cAAc,yBAAyB,CAAC;AACxC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,iBAAiB,CAAC;AAChC,cAAc,qBAAqB,CAAC;AAEpC,cAAc,0BAA0B,CAAC;AACzC,cAAc,wBAAwB,CAAC;AACvC,cAAc,eAAe,CAAC","sourcesContent":["import './register';\n\nexport * from './context/computed-item.context';\nexport * from './context/types/computed-item.types';\nexport * from './context/computed.context';\nexport * from './context/types/computed.types';\nexport * from './context/config.context';\nexport * from './context/interaction.context';\nexport * from './context/item.context';\nexport * from './context/types/item.types';\nexport * from './context/qti.context';\nexport * from './context/session.context';\nexport * from './context/test.context';\n\nexport * from './styles/box-sizing.styles';\nexport * from './styles/drop-region.styles';\nexport * from './styles/validation-message.styles';\n\nexport * from './abstract/interaction';\nexport * from './abstract/qti-variable-declaration';\nexport * from './abstract/qti-condition-expression';\nexport * from './abstract/qti-feedback';\nexport * from './abstract/qti-expression';\n\nexport * from './lib/response';\nexport * from './lib/expression-result';\nexport * from './lib/interaction.interface';\nexport * from './lib/variables';\nexport * from './lib/qti-rule-base';\n\nexport * from './utils/custom-state-set';\nexport * from './utils/scoring-helper';\nexport * from './utils/utils';\n"]}
package/dist/register.js CHANGED
@@ -1,4 +1,8 @@
1
1
  import { qtiBaseElements } from './elements';
2
+ import { installCustomStateSetFallback } from './utils/custom-state-set';
3
+ // Before any element is defined, so the patched `attachInternals` is in place by
4
+ // the time the first interaction upgrades. No-op where `states` works natively.
5
+ installCustomStateSetFallback();
2
6
  for (const { tag, ctor } of qtiBaseElements) {
3
7
  if (!customElements.get(tag)) {
4
8
  customElements.define(tag, ctor);
@@ -1 +1 @@
1
- {"version":3,"file":"register.js","sourceRoot":"","sources":["../src/register.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAE7C,KAAK,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,IAAI,eAAe,EAAE,CAAC;IAC5C,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;QAC7B,cAAc,CAAC,MAAM,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;IACnC,CAAC;AACH,CAAC","sourcesContent":["import { qtiBaseElements } from './elements';\n\nfor (const { tag, ctor } of qtiBaseElements) {\n if (!customElements.get(tag)) {\n customElements.define(tag, ctor);\n }\n}\n"]}
1
+ {"version":3,"file":"register.js","sourceRoot":"","sources":["../src/register.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAC7C,OAAO,EAAE,6BAA6B,EAAE,MAAM,0BAA0B,CAAC;AAEzE,iFAAiF;AACjF,gFAAgF;AAChF,6BAA6B,EAAE,CAAC;AAEhC,KAAK,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,IAAI,eAAe,EAAE,CAAC;IAC5C,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;QAC7B,cAAc,CAAC,MAAM,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;IACnC,CAAC;AACH,CAAC","sourcesContent":["import { qtiBaseElements } from './elements';\nimport { installCustomStateSetFallback } from './utils/custom-state-set';\n\n// Before any element is defined, so the patched `attachInternals` is in place by\n// the time the first interaction upgrades. No-op where `states` works natively.\ninstallCustomStateSetFallback();\n\nfor (const { tag, ctor } of qtiBaseElements) {\n if (!customElements.get(tag)) {\n customElements.define(tag, ctor);\n }\n}\n"]}
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Custom-state support shim for browsers this library cannot use natively.
3
+ *
4
+ * Interactions track selection and correct/incorrect marking through
5
+ * `internals.states`, with bare state names (`checked`, `radio`,
6
+ * `correct-response`, …). Two browser profiles cannot service that:
7
+ *
8
+ * - Safari 16.4–17.3 implement `ElementInternals` but not `CustomStateSet`, so
9
+ * `internals.states` is `undefined` and reading it throws — "undefined is not
10
+ * an object (evaluating 'internals.states.has')".
11
+ * - Chrome and Edge before the CSS custom-state spec change expose `states` but
12
+ * reject names that do not start with `--`, so `states.add('radio')` throws
13
+ * "The specified value 'radio' must start with '--'".
14
+ *
15
+ * On both, picking a choice throws rather than registering. `attachInternals`
16
+ * itself is unguarded throughout the interaction base classes, which puts the
17
+ * hard support floor at Safari 16.4 — inside the range that breaks.
18
+ *
19
+ * The shim replaces `states` with a permissive `Set` and mirrors its contents to
20
+ * a space-separated `data-state` attribute on the host, because these browsers'
21
+ * CSS parsers also drop any selector list containing `:state()`. The theme's
22
+ * built stylesheets pair every `:state(x)` with a `[data-state~='x']` arm (see
23
+ * `tools/postcss/custom-state-fallback.mjs`), so styling survives too.
24
+ *
25
+ * Support is classified by behaviour rather than by browser version, and the
26
+ * shim is not installed at all where `states` works.
27
+ */
28
+ /** A permissive `CustomStateSet` that mirrors its contents to `data-state`. */
29
+ export declare class CustomStateSetShim extends Set<string> {
30
+ #private;
31
+ constructor(host: HTMLElement);
32
+ add(state: string): this;
33
+ delete(state: string): boolean;
34
+ clear(): void;
35
+ }
36
+ export type CustomStateSupport = 'missing' | 'legacy' | 'modern';
37
+ /**
38
+ * Classify native custom-state support by behaviour:
39
+ *
40
+ * - `missing`: `ElementInternals` carries no `states` at all.
41
+ * - `legacy`: `states` exists but rejects a bare name.
42
+ * - `modern`: `states` exists and accepts a bare name — no shim wanted.
43
+ *
44
+ * `attachInternals` only works on a custom element, so the probe registers a
45
+ * throwaway one. That happens solely when `states` is present, so the `missing`
46
+ * path never defines anything.
47
+ */
48
+ export declare function classifyCustomStateSupport(attachInternals: (this: HTMLElement) => ElementInternals): CustomStateSupport;
49
+ /**
50
+ * Patch `HTMLElement.prototype.attachInternals` so every element's `states` is
51
+ * the shim, on the two profiles that need it. Idempotent, and a no-op where
52
+ * native support is usable.
53
+ *
54
+ * Returns the classification that was acted on, which is what the tests assert.
55
+ */
56
+ export declare function installCustomStateSetFallback(): CustomStateSupport | 'already-installed';
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Custom-state support shim for browsers this library cannot use natively.
3
+ *
4
+ * Interactions track selection and correct/incorrect marking through
5
+ * `internals.states`, with bare state names (`checked`, `radio`,
6
+ * `correct-response`, …). Two browser profiles cannot service that:
7
+ *
8
+ * - Safari 16.4–17.3 implement `ElementInternals` but not `CustomStateSet`, so
9
+ * `internals.states` is `undefined` and reading it throws — "undefined is not
10
+ * an object (evaluating 'internals.states.has')".
11
+ * - Chrome and Edge before the CSS custom-state spec change expose `states` but
12
+ * reject names that do not start with `--`, so `states.add('radio')` throws
13
+ * "The specified value 'radio' must start with '--'".
14
+ *
15
+ * On both, picking a choice throws rather than registering. `attachInternals`
16
+ * itself is unguarded throughout the interaction base classes, which puts the
17
+ * hard support floor at Safari 16.4 — inside the range that breaks.
18
+ *
19
+ * The shim replaces `states` with a permissive `Set` and mirrors its contents to
20
+ * a space-separated `data-state` attribute on the host, because these browsers'
21
+ * CSS parsers also drop any selector list containing `:state()`. The theme's
22
+ * built stylesheets pair every `:state(x)` with a `[data-state~='x']` arm (see
23
+ * `tools/postcss/custom-state-fallback.mjs`), so styling survives too.
24
+ *
25
+ * Support is classified by behaviour rather than by browser version, and the
26
+ * shim is not installed at all where `states` works.
27
+ */
28
+ /** A permissive `CustomStateSet` that mirrors its contents to `data-state`. */
29
+ export class CustomStateSetShim extends Set {
30
+ #host;
31
+ constructor(host) {
32
+ super();
33
+ this.#host = host;
34
+ }
35
+ add(state) {
36
+ super.add(state);
37
+ this.#reflect();
38
+ return this;
39
+ }
40
+ delete(state) {
41
+ const deleted = super.delete(state);
42
+ this.#reflect();
43
+ return deleted;
44
+ }
45
+ clear() {
46
+ super.clear();
47
+ this.#reflect();
48
+ }
49
+ #reflect() {
50
+ // A custom element must not gain attributes while it is upgrading, so defer
51
+ // the write until the element is connected.
52
+ if (this.#host.isConnected)
53
+ this.#write();
54
+ else
55
+ queueMicrotask(() => this.#write());
56
+ }
57
+ #write() {
58
+ if (this.size === 0)
59
+ this.#host.removeAttribute('data-state');
60
+ else
61
+ this.#host.setAttribute('data-state', Array.from(this).join(' '));
62
+ }
63
+ }
64
+ const PROBE_TAG = 'custom-state-support-probe';
65
+ /**
66
+ * Classify native custom-state support by behaviour:
67
+ *
68
+ * - `missing`: `ElementInternals` carries no `states` at all.
69
+ * - `legacy`: `states` exists but rejects a bare name.
70
+ * - `modern`: `states` exists and accepts a bare name — no shim wanted.
71
+ *
72
+ * `attachInternals` only works on a custom element, so the probe registers a
73
+ * throwaway one. That happens solely when `states` is present, so the `missing`
74
+ * path never defines anything.
75
+ */
76
+ export function classifyCustomStateSupport(attachInternals) {
77
+ if (!('states' in ElementInternals.prototype))
78
+ return 'missing';
79
+ if (typeof customElements === 'undefined')
80
+ return 'modern';
81
+ try {
82
+ if (!customElements.get(PROBE_TAG))
83
+ customElements.define(PROBE_TAG, class extends HTMLElement {
84
+ });
85
+ const states = attachInternals.call(document.createElement(PROBE_TAG)).states;
86
+ if (!states)
87
+ return 'missing';
88
+ // A bare name throws SyntaxError under the legacy spec and never does under
89
+ // the current one, so classify on the throw alone — not on whether the value
90
+ // sticks, which some environments do not honour on a detached element.
91
+ states.add('probe');
92
+ states.delete('probe');
93
+ return 'modern';
94
+ }
95
+ catch {
96
+ return 'legacy';
97
+ }
98
+ }
99
+ let installed = false;
100
+ /**
101
+ * Patch `HTMLElement.prototype.attachInternals` so every element's `states` is
102
+ * the shim, on the two profiles that need it. Idempotent, and a no-op where
103
+ * native support is usable.
104
+ *
105
+ * Returns the classification that was acted on, which is what the tests assert.
106
+ */
107
+ export function installCustomStateSetFallback() {
108
+ if (installed)
109
+ return 'already-installed';
110
+ if (typeof HTMLElement === 'undefined' || typeof ElementInternals === 'undefined')
111
+ return 'modern';
112
+ const native = HTMLElement.prototype.attachInternals;
113
+ if (!native)
114
+ return 'modern';
115
+ const support = classifyCustomStateSupport(native);
116
+ if (support === 'modern')
117
+ return support;
118
+ installed = true;
119
+ HTMLElement.prototype.attachInternals = function attachInternals() {
120
+ const internals = native.call(this);
121
+ Object.defineProperty(internals, 'states', {
122
+ configurable: true,
123
+ enumerable: true,
124
+ value: new CustomStateSetShim(this)
125
+ });
126
+ return internals;
127
+ };
128
+ return support;
129
+ }
130
+ //# sourceMappingURL=custom-state-set.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"custom-state-set.js","sourceRoot":"","sources":["../../src/utils/custom-state-set.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,+EAA+E;AAC/E,MAAM,OAAO,kBAAmB,SAAQ,GAAW;IACxC,KAAK,CAAc;IAE5B,YAAY,IAAiB;QAC3B,KAAK,EAAE,CAAC;QACR,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;IACpB,CAAC;IAEQ,GAAG,CAAC,KAAa;QACxB,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QACjB,IAAI,CAAC,QAAQ,EAAE,CAAC;QAChB,OAAO,IAAI,CAAC;IACd,CAAC;IAEQ,MAAM,CAAC,KAAa;QAC3B,MAAM,OAAO,GAAG,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QACpC,IAAI,CAAC,QAAQ,EAAE,CAAC;QAChB,OAAO,OAAO,CAAC;IACjB,CAAC;IAEQ,KAAK;QACZ,KAAK,CAAC,KAAK,EAAE,CAAC;QACd,IAAI,CAAC,QAAQ,EAAE,CAAC;IAClB,CAAC;IAED,QAAQ;QACN,4EAA4E;QAC5E,4CAA4C;QAC5C,IAAI,IAAI,CAAC,KAAK,CAAC,WAAW;YAAE,IAAI,CAAC,MAAM,EAAE,CAAC;;YACrC,cAAc,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;IAC3C,CAAC;IAED,MAAM;QACJ,IAAI,IAAI,CAAC,IAAI,KAAK,CAAC;YAAE,IAAI,CAAC,KAAK,CAAC,eAAe,CAAC,YAAY,CAAC,CAAC;;YACzD,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,YAAY,EAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;IACzE,CAAC;CACF;AAID,MAAM,SAAS,GAAG,4BAA4B,CAAC;AAE/C;;;;;;;;;;GAUG;AACH,MAAM,UAAU,0BAA0B,CACxC,eAAwD;IAExD,IAAI,CAAC,CAAC,QAAQ,IAAI,gBAAgB,CAAC,SAAS,CAAC;QAAE,OAAO,SAAS,CAAC;IAChE,IAAI,OAAO,cAAc,KAAK,WAAW;QAAE,OAAO,QAAQ,CAAC;IAE3D,IAAI,CAAC;QACH,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,SAAS,CAAC;YAAE,cAAc,CAAC,MAAM,CAAC,SAAS,EAAE,KAAM,SAAQ,WAAW;aAAG,CAAC,CAAC;QACnG,MAAM,MAAM,GAAG,eAAe,CAAC,IAAI,CAAC,QAAQ,CAAC,aAAa,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC;QAC9E,IAAI,CAAC,MAAM;YAAE,OAAO,SAAS,CAAC;QAC9B,4EAA4E;QAC5E,6EAA6E;QAC7E,uEAAuE;QACvE,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QACpB,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;QACvB,OAAO,QAAQ,CAAC;IAClB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,QAAQ,CAAC;IAClB,CAAC;AACH,CAAC;AAED,IAAI,SAAS,GAAG,KAAK,CAAC;AAEtB;;;;;;GAMG;AACH,MAAM,UAAU,6BAA6B;IAC3C,IAAI,SAAS;QAAE,OAAO,mBAAmB,CAAC;IAC1C,IAAI,OAAO,WAAW,KAAK,WAAW,IAAI,OAAO,gBAAgB,KAAK,WAAW;QAAE,OAAO,QAAQ,CAAC;IAEnG,MAAM,MAAM,GAAG,WAAW,CAAC,SAAS,CAAC,eAAe,CAAC;IACrD,IAAI,CAAC,MAAM;QAAE,OAAO,QAAQ,CAAC;IAE7B,MAAM,OAAO,GAAG,0BAA0B,CAAC,MAAM,CAAC,CAAC;IACnD,IAAI,OAAO,KAAK,QAAQ;QAAE,OAAO,OAAO,CAAC;IAEzC,SAAS,GAAG,IAAI,CAAC;IACjB,WAAW,CAAC,SAAS,CAAC,eAAe,GAAG,SAAS,eAAe;QAC9D,MAAM,SAAS,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACpC,MAAM,CAAC,cAAc,CAAC,SAAS,EAAE,QAAQ,EAAE;YACzC,YAAY,EAAE,IAAI;YAClB,UAAU,EAAE,IAAI;YAChB,KAAK,EAAE,IAAI,kBAAkB,CAAC,IAAI,CAAC;SACpC,CAAC,CAAC;QACH,OAAO,SAAS,CAAC;IACnB,CAAC,CAAC;IAEF,OAAO,OAAO,CAAC;AACjB,CAAC","sourcesContent":["/**\n * Custom-state support shim for browsers this library cannot use natively.\n *\n * Interactions track selection and correct/incorrect marking through\n * `internals.states`, with bare state names (`checked`, `radio`,\n * `correct-response`, …). Two browser profiles cannot service that:\n *\n * - Safari 16.4–17.3 implement `ElementInternals` but not `CustomStateSet`, so\n * `internals.states` is `undefined` and reading it throws — \"undefined is not\n * an object (evaluating 'internals.states.has')\".\n * - Chrome and Edge before the CSS custom-state spec change expose `states` but\n * reject names that do not start with `--`, so `states.add('radio')` throws\n * \"The specified value 'radio' must start with '--'\".\n *\n * On both, picking a choice throws rather than registering. `attachInternals`\n * itself is unguarded throughout the interaction base classes, which puts the\n * hard support floor at Safari 16.4 — inside the range that breaks.\n *\n * The shim replaces `states` with a permissive `Set` and mirrors its contents to\n * a space-separated `data-state` attribute on the host, because these browsers'\n * CSS parsers also drop any selector list containing `:state()`. The theme's\n * built stylesheets pair every `:state(x)` with a `[data-state~='x']` arm (see\n * `tools/postcss/custom-state-fallback.mjs`), so styling survives too.\n *\n * Support is classified by behaviour rather than by browser version, and the\n * shim is not installed at all where `states` works.\n */\n\n/** A permissive `CustomStateSet` that mirrors its contents to `data-state`. */\nexport class CustomStateSetShim extends Set<string> {\n readonly #host: HTMLElement;\n\n constructor(host: HTMLElement) {\n super();\n this.#host = host;\n }\n\n override add(state: string): this {\n super.add(state);\n this.#reflect();\n return this;\n }\n\n override delete(state: string): boolean {\n const deleted = super.delete(state);\n this.#reflect();\n return deleted;\n }\n\n override clear(): void {\n super.clear();\n this.#reflect();\n }\n\n #reflect(): void {\n // A custom element must not gain attributes while it is upgrading, so defer\n // the write until the element is connected.\n if (this.#host.isConnected) this.#write();\n else queueMicrotask(() => this.#write());\n }\n\n #write(): void {\n if (this.size === 0) this.#host.removeAttribute('data-state');\n else this.#host.setAttribute('data-state', Array.from(this).join(' '));\n }\n}\n\nexport type CustomStateSupport = 'missing' | 'legacy' | 'modern';\n\nconst PROBE_TAG = 'custom-state-support-probe';\n\n/**\n * Classify native custom-state support by behaviour:\n *\n * - `missing`: `ElementInternals` carries no `states` at all.\n * - `legacy`: `states` exists but rejects a bare name.\n * - `modern`: `states` exists and accepts a bare name — no shim wanted.\n *\n * `attachInternals` only works on a custom element, so the probe registers a\n * throwaway one. That happens solely when `states` is present, so the `missing`\n * path never defines anything.\n */\nexport function classifyCustomStateSupport(\n attachInternals: (this: HTMLElement) => ElementInternals\n): CustomStateSupport {\n if (!('states' in ElementInternals.prototype)) return 'missing';\n if (typeof customElements === 'undefined') return 'modern';\n\n try {\n if (!customElements.get(PROBE_TAG)) customElements.define(PROBE_TAG, class extends HTMLElement {});\n const states = attachInternals.call(document.createElement(PROBE_TAG)).states;\n if (!states) return 'missing';\n // A bare name throws SyntaxError under the legacy spec and never does under\n // the current one, so classify on the throw alone — not on whether the value\n // sticks, which some environments do not honour on a detached element.\n states.add('probe');\n states.delete('probe');\n return 'modern';\n } catch {\n return 'legacy';\n }\n}\n\nlet installed = false;\n\n/**\n * Patch `HTMLElement.prototype.attachInternals` so every element's `states` is\n * the shim, on the two profiles that need it. Idempotent, and a no-op where\n * native support is usable.\n *\n * Returns the classification that was acted on, which is what the tests assert.\n */\nexport function installCustomStateSetFallback(): CustomStateSupport | 'already-installed' {\n if (installed) return 'already-installed';\n if (typeof HTMLElement === 'undefined' || typeof ElementInternals === 'undefined') return 'modern';\n\n const native = HTMLElement.prototype.attachInternals;\n if (!native) return 'modern';\n\n const support = classifyCustomStateSupport(native);\n if (support === 'modern') return support;\n\n installed = true;\n HTMLElement.prototype.attachInternals = function attachInternals(this: HTMLElement): ElementInternals {\n const internals = native.call(this);\n Object.defineProperty(internals, 'states', {\n configurable: true,\n enumerable: true,\n value: new CustomStateSetShim(this)\n });\n return internals;\n };\n\n return support;\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.0.1",
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.2"
11
+ "lit": "^3.3.3"
12
12
  },
13
13
  "keywords": [
14
14
  "qti",