@citolab/tspci 2.6.3-beta.1 → 2.6.8
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/CHANGELOG.md +48 -0
- package/package.json +21 -30
- package/preset/javascript/src/index.js +130 -109
- package/preset/preact/src/index.tsx +109 -66
- package/preset/typescript/src/index.ts +146 -57
- package/readme.md +31 -34
- package/src/rollup-plugin-live-server.js +1 -1
- package/types/index.d.ts +11 -13
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,54 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
## [2.6.8](/compare/v2.6.7...v2.6.8) (2026-02-02)
|
|
7
|
+
|
|
8
|
+
**Note:** Version bump only for package @citolab/tspci
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
## [2.6.7](/compare/v2.6.6...v2.6.7) (2026-02-02)
|
|
15
|
+
|
|
16
|
+
**Note:** Version bump only for package @citolab/tspci
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
## [2.6.6](/compare/v2.6.5...v2.6.6) (2026-02-02)
|
|
23
|
+
|
|
24
|
+
**Note:** Version bump only for package @citolab/tspci
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
## [2.6.5](/compare/v2.6.3-beta.3...v2.6.5) (2025-04-16)
|
|
31
|
+
|
|
32
|
+
**Note:** Version bump only for package @citolab/tspci
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
## [2.6.3-beta.3](/compare/v2.6.3-beta.2...v2.6.3-beta.3) (2025-04-16)
|
|
39
|
+
|
|
40
|
+
**Note:** Version bump only for package @citolab/tspci
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
## [2.6.3-beta.2](/compare/v2.6.3-beta.1...v2.6.3-beta.2) (2025-04-16)
|
|
47
|
+
|
|
48
|
+
**Note:** Version bump only for package @citolab/tspci
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
|
|
6
54
|
## [2.6.3-beta.1](/compare/v2.6.3-beta.0...v2.6.3-beta.1) (2025-04-14)
|
|
7
55
|
|
|
8
56
|
**Note:** Version bump only for package @citolab/tspci
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@citolab/tspci",
|
|
3
|
-
"version": "2.6.
|
|
3
|
+
"version": "2.6.8",
|
|
4
4
|
"description": "an opiniated typescript ims-pci builder with tao integration",
|
|
5
5
|
"author": "patrick <patrick.deklein@cito.nl>",
|
|
6
6
|
"repository": "@citolab/tspci",
|
|
@@ -14,42 +14,33 @@
|
|
|
14
14
|
"type": "module",
|
|
15
15
|
"types": "types/index.d.ts",
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@
|
|
18
|
-
"@rollup/plugin-
|
|
19
|
-
"@rollup/plugin-commonjs": "^
|
|
17
|
+
"@compodoc/live-server": "^1.2.3",
|
|
18
|
+
"@rollup/plugin-alias": "^6.0.0",
|
|
19
|
+
"@rollup/plugin-commonjs": "^29.0.0",
|
|
20
20
|
"@rollup/plugin-image": "^3.0.3",
|
|
21
21
|
"@rollup/plugin-json": "^6.1.0",
|
|
22
|
-
"@rollup/plugin-node-resolve": "^
|
|
23
|
-
"@rollup/plugin-replace": "^6.0.
|
|
22
|
+
"@rollup/plugin-node-resolve": "^16.0.3",
|
|
23
|
+
"@rollup/plugin-replace": "^6.0.3",
|
|
24
24
|
"@rollup/plugin-terser": "^0.4.4",
|
|
25
|
-
"@rollup/plugin-typescript": "^12.
|
|
25
|
+
"@rollup/plugin-typescript": "^12.3.0",
|
|
26
26
|
"@tailwindcss/nesting": "0.0.0-insiders.565cd3e",
|
|
27
|
-
"@tailwindcss/postcss": "^4.
|
|
28
|
-
"@
|
|
29
|
-
"@
|
|
30
|
-
"@typescript-eslint/
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
"chokidar": "^4.0.0",
|
|
27
|
+
"@tailwindcss/postcss": "^4.1.18",
|
|
28
|
+
"@types/node": "^25.1.0",
|
|
29
|
+
"@typescript-eslint/eslint-plugin": "^8.54.0",
|
|
30
|
+
"@typescript-eslint/parser": "^8.54.0",
|
|
31
|
+
"autoprefixer": "10.4.23",
|
|
32
|
+
"chalk": "5.6.2",
|
|
33
|
+
"chokidar": "^5.0.0",
|
|
35
34
|
"clear": "0.1.0",
|
|
36
|
-
"figlet": "1.
|
|
37
|
-
"inquirer": "
|
|
38
|
-
"
|
|
39
|
-
"postcss": "^
|
|
40
|
-
"
|
|
41
|
-
"rollup": "^4.34.1",
|
|
35
|
+
"figlet": "1.10.0",
|
|
36
|
+
"inquirer": "13.2.2",
|
|
37
|
+
"postcss": "^8.5.6",
|
|
38
|
+
"postcss-import": "^16.1.1",
|
|
39
|
+
"rollup": "^4.57.1",
|
|
42
40
|
"rollup-plugin-copy": "^3.5.0",
|
|
43
|
-
"rollup-plugin-live-server": "^2.0.0",
|
|
44
41
|
"rollup-plugin-livereload": "^2.0.5",
|
|
45
|
-
"rollup-plugin-node-builtins": "^2.1.2",
|
|
46
|
-
"rollup-plugin-node-globals": "^1.4.0",
|
|
47
42
|
"rollup-plugin-postcss": "^4.0.2",
|
|
48
|
-
"
|
|
49
|
-
"tailwindcss": "^4.1.3"
|
|
43
|
+
"tailwindcss": "^4.1.18"
|
|
50
44
|
},
|
|
51
|
-
"
|
|
52
|
-
"chokidar": "^4.0.0"
|
|
53
|
-
},
|
|
54
|
-
"gitHead": "86cdeaf19f50955e56c729ea9fe96ab82d1df171"
|
|
45
|
+
"gitHead": "8ee09d71856222417e1fc33d22edaf05344c68fe"
|
|
55
46
|
}
|
|
@@ -2,144 +2,165 @@ import * as ctx from "qtiCustomInteractionContext";
|
|
|
2
2
|
import configProps from "./config.json";
|
|
3
3
|
import style from "./styles.css"; // import and bundle this style file ( you can use tailwind and nested css )
|
|
4
4
|
import procenten from "./assets/procenten.png"; // image types are bundled inside the js
|
|
5
|
-
class
|
|
6
|
-
|
|
7
|
-
* @constructor
|
|
8
|
-
* @param {string} typeIdentifier - The unique identifier for this PCI type.
|
|
9
|
-
*/
|
|
10
|
-
typeIdentifier; // typeIdentifier is mandatory for all PCI's
|
|
11
|
-
config; // reference to the interface of the config object which you get when getInstance is called by the player
|
|
12
|
-
state; // keep a reference to the state
|
|
13
|
-
shadowdom; // Not mandatory, but its wise to create a shadowroot
|
|
14
|
-
|
|
15
|
-
constructor() {
|
|
16
|
-
/**
|
|
17
|
-
* @type {string}
|
|
18
|
-
* @description The unique identifier for this PCI type.
|
|
19
|
-
*/
|
|
20
|
-
this.typeIdentifier = "###___PCI_NAME___###"; // typeIdentifier is mandatory for all PCI's
|
|
21
|
-
if (ctx) {
|
|
22
|
-
ctx.register(this); // we assume the qtiCustomInteractionContext is avaible due to the import above
|
|
23
|
-
}
|
|
24
|
-
}
|
|
5
|
+
class PciInstance {
|
|
6
|
+
typeIdentifier = "###___PCI_NAME___###";
|
|
25
7
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
*
|
|
31
|
-
* @param {HTMLElement} dom - The DOM element to which this PCI is added.
|
|
32
|
-
* @param {Object<ConfigProperties>} config - The configuration to apply to this PCI.
|
|
33
|
-
* @param {string} state - A previously saved state to restore the PCI instance.
|
|
34
|
-
* This state must have been obtained from a prior call to getState for an instance of the same typeIdentifier.
|
|
35
|
-
*/
|
|
36
|
-
getInstance = (dom, config, state) => {
|
|
37
|
-
config.properties = { ...configProps, ...config.properties }; // merge current props with incoming
|
|
8
|
+
constructor(dom, config, stateString) {
|
|
9
|
+
if (!dom) throw new Error("No DOM Element provided");
|
|
10
|
+
config.properties = { ...configProps, ...config.properties };
|
|
11
|
+
this.dom = dom;
|
|
38
12
|
this.config = config;
|
|
39
|
-
this.state = state ? state : "";
|
|
40
|
-
|
|
41
13
|
this.shadowdom = dom.attachShadow({ mode: "closed" });
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
if (
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
14
|
+
|
|
15
|
+
this.state = { value: "", touched: false, customValidity: "" };
|
|
16
|
+
if (stateString) {
|
|
17
|
+
try {
|
|
18
|
+
const parsed = JSON.parse(stateString);
|
|
19
|
+
this.state.value = typeof parsed?.value === "string" ? parsed.value : "";
|
|
20
|
+
this.state.touched = parsed?.touched === true;
|
|
21
|
+
this.state.customValidity =
|
|
22
|
+
typeof parsed?.customValidity === "string" ? parsed.customValidity : "";
|
|
23
|
+
} catch {
|
|
24
|
+
this.state.value = String(stateString);
|
|
25
|
+
this.state.touched = false;
|
|
26
|
+
}
|
|
48
27
|
}
|
|
49
28
|
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
29
|
+
const boundResponse =
|
|
30
|
+
(this.config.boundTo && this.config.responseIdentifier
|
|
31
|
+
? this.config.boundTo[this.config.responseIdentifier]
|
|
32
|
+
: undefined) ||
|
|
33
|
+
(this.config.boundTo && Object.keys(this.config.boundTo).length === 1
|
|
34
|
+
? this.config.boundTo[Object.keys(this.config.boundTo)[0]]
|
|
35
|
+
: undefined);
|
|
36
|
+
if (boundResponse) {
|
|
37
|
+
this.setResponse(boundResponse);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
this.onChange = (e) => {
|
|
41
|
+
const target = e && e.target;
|
|
42
|
+
if (!target) return;
|
|
43
|
+
const nextValue = typeof target.value === "string" ? target.value : "";
|
|
44
|
+
if (nextValue === this.state.value && this.state.touched) return;
|
|
45
|
+
this.state.value = nextValue;
|
|
46
|
+
this.state.touched = true;
|
|
47
|
+
this.dispatchInteractionChanged();
|
|
55
48
|
};
|
|
56
|
-
// dispatch a custom event to notify the Delivery System that the interaction has changed
|
|
57
|
-
const interactionChangedEvent = new CustomEvent("qtiInteractionChanged", {
|
|
58
|
-
detail: event,
|
|
59
|
-
});
|
|
60
|
-
this.shadowdom.dispatchEvent(interactionChangedEvent);
|
|
61
|
-
};
|
|
62
49
|
|
|
63
|
-
|
|
64
|
-
|
|
50
|
+
this.render();
|
|
51
|
+
|
|
52
|
+
if (this.config.onready) {
|
|
53
|
+
this.config.onready(this, this.getState());
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
getInputEl = () => this.shadowdom.querySelector("input[type='number']");
|
|
58
|
+
|
|
59
|
+
dispatchInteractionChanged = () => {
|
|
60
|
+
const detail = {
|
|
65
61
|
interaction: this,
|
|
66
62
|
responseIdentifier: this.config.responseIdentifier,
|
|
67
|
-
valid: true,
|
|
63
|
+
valid: this.checkValidity?.() ?? true,
|
|
68
64
|
value: this.getResponse(),
|
|
69
65
|
};
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
66
|
+
const event = new CustomEvent("qti-interaction-changed", {
|
|
67
|
+
bubbles: true,
|
|
68
|
+
cancelable: true,
|
|
69
|
+
detail,
|
|
73
70
|
});
|
|
74
|
-
this.dom.dispatchEvent(
|
|
71
|
+
this.dom.dispatchEvent(event);
|
|
75
72
|
};
|
|
76
73
|
|
|
77
|
-
setResponse = (response) => {
|
|
78
|
-
// TODO restore the response
|
|
79
|
-
// Get the actual value by the variable type like:
|
|
80
|
-
// - response?.base?.string or response?.base?.integer
|
|
81
|
-
}
|
|
82
|
-
|
|
83
74
|
render = () => {
|
|
84
|
-
this.shadowdom.addEventListener("change", (e) => {
|
|
85
|
-
const value = e.target.value;
|
|
86
|
-
this.state = value;
|
|
87
|
-
this.interactionChanged();
|
|
88
|
-
});
|
|
89
75
|
this.shadowdom.innerHTML = `<div class="pci-container">
|
|
90
76
|
<h1>${this.config.properties.title}</h1>
|
|
91
77
|
<div class="body">
|
|
92
|
-
<img width="${+this.config.properties.width}" height="${+this.config
|
|
78
|
+
<img width="${+this.config.properties.width}" height="${+this.config
|
|
79
|
+
.properties.height}" src="${procenten}" />
|
|
93
80
|
</div>
|
|
94
81
|
<div class="interaction">
|
|
95
|
-
<label
|
|
96
|
-
<input type="number" value="${this.state}" min="0" max="100">%
|
|
82
|
+
<label>${this.config.properties.prompt}</label>
|
|
83
|
+
<input type="number" value="${this.state.value}" min="0" max="100">%
|
|
97
84
|
</div>
|
|
98
85
|
</div>`;
|
|
99
86
|
const css = document.createElement("style");
|
|
100
87
|
css.innerHTML = style;
|
|
101
88
|
this.shadowdom.appendChild(css);
|
|
89
|
+
|
|
90
|
+
const input = this.getInputEl();
|
|
91
|
+
if (input) {
|
|
92
|
+
if (this.state.customValidity) input.setCustomValidity(this.state.customValidity);
|
|
93
|
+
input.addEventListener("input", this.onChange);
|
|
94
|
+
input.addEventListener("change", this.onChange);
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
setResponse = (response) => {
|
|
99
|
+
let next = "";
|
|
100
|
+
if (response && response.base) {
|
|
101
|
+
if (typeof response.base.integer === "number") next = String(response.base.integer);
|
|
102
|
+
else if (typeof response.base.string === "string") next = response.base.string;
|
|
103
|
+
}
|
|
104
|
+
this.state.value = next;
|
|
105
|
+
this.state.touched = true;
|
|
106
|
+
const input = this.getInputEl();
|
|
107
|
+
if (input) input.value = next;
|
|
102
108
|
};
|
|
103
109
|
|
|
104
|
-
/**
|
|
105
|
-
* @method getResponse
|
|
106
|
-
* @description Retrieves the value to assign to the bound QTI response variable.
|
|
107
|
-
*
|
|
108
|
-
* @returns {Object} - The response value.
|
|
109
|
-
*/
|
|
110
110
|
getResponse = () => {
|
|
111
|
-
|
|
112
|
-
|
|
111
|
+
if (!this.state.touched) return undefined;
|
|
112
|
+
const trimmed = String(this.state.value == null ? "" : this.state.value).trim();
|
|
113
|
+
if (!trimmed) return undefined;
|
|
114
|
+
const asNumber = Number(trimmed);
|
|
115
|
+
if (!Number.isFinite(asNumber)) return undefined;
|
|
116
|
+
return { base: { integer: Math.trunc(asNumber) } };
|
|
113
117
|
};
|
|
114
118
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
119
|
+
getState = () => JSON.stringify(this.state);
|
|
120
|
+
|
|
121
|
+
checkValidity = () => {
|
|
122
|
+
const input = this.getInputEl();
|
|
123
|
+
if (!input) return undefined;
|
|
124
|
+
return input.checkValidity();
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
reportValidity = () => {
|
|
128
|
+
const input = this.getInputEl();
|
|
129
|
+
if (!input) return undefined;
|
|
130
|
+
return input.reportValidity();
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
setCustomValidity = (message) => {
|
|
134
|
+
this.state.customValidity = message == null ? "" : String(message);
|
|
135
|
+
const input = this.getInputEl();
|
|
136
|
+
if (input) input.setCustomValidity(this.state.customValidity);
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
getCustomValidity = () => this.state.customValidity ?? "";
|
|
140
|
+
|
|
141
|
+
oncompleted = () => {
|
|
142
|
+
const input = this.getInputEl();
|
|
143
|
+
if (input) {
|
|
144
|
+
input.removeEventListener("input", this.onChange);
|
|
145
|
+
input.removeEventListener("change", this.onChange);
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const factory = {
|
|
151
|
+
typeIdentifier: "###___PCI_NAME___###",
|
|
152
|
+
getInstance: (dom, config, state) => new PciInstance(dom, config, state),
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const engineContext =
|
|
156
|
+
ctx && typeof ctx.register === "function"
|
|
157
|
+
? ctx
|
|
158
|
+
: typeof window !== "undefined" && window.qtiCustomInteractionContext
|
|
159
|
+
? window.qtiCustomInteractionContext
|
|
160
|
+
: undefined;
|
|
161
|
+
|
|
162
|
+
if (engineContext && typeof engineContext.register === "function") {
|
|
163
|
+
engineContext.register(factory);
|
|
143
164
|
}
|
|
144
165
|
|
|
145
|
-
export default
|
|
166
|
+
export default factory;
|
|
@@ -1,77 +1,97 @@
|
|
|
1
1
|
import "preact/debug";
|
|
2
2
|
import { render } from "preact";
|
|
3
|
-
import { IStore } from "@citolab/preact-store";
|
|
3
|
+
import { IStore, Listener } from "@citolab/preact-store";
|
|
4
4
|
import * as ctx from "qtiCustomInteractionContext";
|
|
5
5
|
import Interaction from "./interaction";
|
|
6
6
|
import style from "./styles.css";
|
|
7
|
-
import { ConfigProperties, IMSpci, QtiInteractionChangedDetail, QtiVariableJSON } from "@citolab/tspci";
|
|
7
|
+
import { ConfigProperties, IMSpci, IMSpciFactory, QtiInteractionChangedDetail, QtiVariableJSON } from "@citolab/tspci";
|
|
8
8
|
import configProps from "./config.json";
|
|
9
9
|
import { StateModel, initStore } from "./store";
|
|
10
10
|
|
|
11
11
|
type PropTypes = typeof configProps;
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
private
|
|
21
|
-
private
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
13
|
+
type PciStoredState = {
|
|
14
|
+
state: StateModel;
|
|
15
|
+
log: { type: string; payload: unknown; timestamp?: number }[];
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
class PciInstance implements IMSpci<PropTypes> {
|
|
19
|
+
typeIdentifier = "###___PCI_NAME___###";
|
|
20
|
+
private config: ConfigProperties<PropTypes>;
|
|
21
|
+
private dom: HTMLElement;
|
|
22
|
+
private shadowdom: ShadowRoot;
|
|
23
|
+
private store: IStore<StateModel>;
|
|
24
|
+
private storeListener: Listener<StateModel>;
|
|
25
|
+
private previousResponseJson: string = "";
|
|
26
|
+
private initialState: StateModel = { input: undefined };
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
constructor(dom: HTMLElement, config: ConfigProperties<PropTypes>, stateString?: string) {
|
|
29
|
+
if (!dom) throw new Error("No DOM Element provided");
|
|
30
|
+
config.properties = { ...configProps, ...config.properties };
|
|
31
|
+
this.dom = dom;
|
|
30
32
|
this.config = config;
|
|
31
|
-
|
|
32
|
-
|
|
33
|
+
this.shadowdom = dom.attachShadow({ mode: "closed" });
|
|
34
|
+
|
|
35
|
+
let restoreData: PciStoredState | undefined;
|
|
36
|
+
if (stateString) {
|
|
37
|
+
try {
|
|
38
|
+
const parsed = JSON.parse(stateString) as Partial<PciStoredState>;
|
|
39
|
+
if (parsed && parsed.state && Array.isArray(parsed.log)) {
|
|
40
|
+
restoreData = { state: parsed.state as StateModel, log: parsed.log as PciStoredState["log"] };
|
|
41
|
+
}
|
|
42
|
+
} catch {
|
|
43
|
+
// ignore
|
|
44
|
+
}
|
|
33
45
|
}
|
|
34
|
-
|
|
46
|
+
|
|
35
47
|
this.store = initStore(this.initialState);
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
if (restoredState) {
|
|
39
|
-
this.store.restoreState(restoredState?.state, this.logActions);
|
|
40
|
-
}
|
|
41
|
-
} catch (error) {
|
|
42
|
-
console.log(error);
|
|
48
|
+
if (restoreData) {
|
|
49
|
+
this.store.restoreState(restoreData.state, restoreData.log);
|
|
43
50
|
}
|
|
44
51
|
|
|
45
|
-
|
|
52
|
+
const boundResponse =
|
|
53
|
+
this.config.boundTo?.[this.config.responseIdentifier] ??
|
|
54
|
+
(this.config.boundTo && Object.keys(this.config.boundTo).length === 1
|
|
55
|
+
? this.config.boundTo[Object.keys(this.config.boundTo)[0]]
|
|
56
|
+
: undefined);
|
|
57
|
+
if (boundResponse) {
|
|
58
|
+
this.setResponse?.(boundResponse);
|
|
59
|
+
}
|
|
46
60
|
|
|
47
|
-
this.
|
|
48
|
-
|
|
61
|
+
this.render();
|
|
62
|
+
|
|
63
|
+
if (this.config.onready) {
|
|
64
|
+
this.config.onready(this, this.getState());
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
let isFirst = true;
|
|
68
|
+
this.storeListener = () => {
|
|
69
|
+
if (isFirst) {
|
|
70
|
+
isFirst = false;
|
|
71
|
+
this.previousResponseJson = JSON.stringify(this.getResponse() ?? null);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const nextJson = JSON.stringify(this.getResponse() ?? null);
|
|
75
|
+
if (nextJson === this.previousResponseJson) return;
|
|
76
|
+
this.previousResponseJson = nextJson;
|
|
77
|
+
|
|
78
|
+
const detail: QtiInteractionChangedDetail = {
|
|
49
79
|
interaction: this,
|
|
50
80
|
responseIdentifier: this.config.responseIdentifier,
|
|
51
81
|
valid: true,
|
|
52
82
|
value: this.getResponse(),
|
|
53
83
|
};
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
84
|
+
const evt = new CustomEvent("qti-interaction-changed", {
|
|
85
|
+
bubbles: true,
|
|
86
|
+
cancelable: true,
|
|
87
|
+
detail,
|
|
57
88
|
});
|
|
58
|
-
dom.dispatchEvent(
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
if (this.config.boundTo && Object.keys(this.config.boundTo).length > 0) {
|
|
64
|
-
const responseIdentifier = Object.keys(this.config.boundTo)[0];
|
|
65
|
-
const response = this.config.boundTo[responseIdentifier];
|
|
66
|
-
this.setResponse(response);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
if (this.config.onready) {
|
|
70
|
-
this.config.onready(this);
|
|
71
|
-
}
|
|
72
|
-
};
|
|
89
|
+
this.dom.dispatchEvent(evt);
|
|
90
|
+
};
|
|
91
|
+
this.store.subscribe(this.storeListener);
|
|
92
|
+
}
|
|
73
93
|
|
|
74
|
-
render = () => {
|
|
94
|
+
private render = () => {
|
|
75
95
|
render(null, this.shadowdom);
|
|
76
96
|
const css = document.createElement("style");
|
|
77
97
|
css.innerHTML = style;
|
|
@@ -80,28 +100,51 @@ class App implements IMSpci<PropTypes> {
|
|
|
80
100
|
};
|
|
81
101
|
|
|
82
102
|
setResponse = (response: QtiVariableJSON) => {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
103
|
+
const next =
|
|
104
|
+
typeof response?.base?.integer === "number"
|
|
105
|
+
? response.base.integer
|
|
106
|
+
: typeof response?.base?.string === "string"
|
|
107
|
+
? Number(response.base.string)
|
|
108
|
+
: undefined;
|
|
109
|
+
if (typeof next !== "number" || !Number.isFinite(next)) return;
|
|
110
|
+
this.store.dispatch({ type: "SET_INPUT", payload: { input: Math.trunc(next) } });
|
|
111
|
+
};
|
|
88
112
|
|
|
89
113
|
getState = () =>
|
|
90
114
|
JSON.stringify({
|
|
91
115
|
state: this.store.getState(),
|
|
92
116
|
log: this.store.getActions(),
|
|
93
|
-
});
|
|
117
|
+
} satisfies PciStoredState);
|
|
94
118
|
|
|
95
119
|
getResponse = () => {
|
|
96
|
-
const
|
|
97
|
-
if (
|
|
98
|
-
return {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
120
|
+
const value = this.store.getState().input;
|
|
121
|
+
if (value === undefined) return undefined;
|
|
122
|
+
return { base: { integer: value } } as QtiVariableJSON;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
oncompleted = () => {
|
|
126
|
+
if (this.storeListener) {
|
|
127
|
+
this.store.unsubscribe(this.storeListener);
|
|
128
|
+
}
|
|
129
|
+
render(null, this.shadowdom);
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const factory: IMSpciFactory<PropTypes> = {
|
|
134
|
+
typeIdentifier: "###___PCI_NAME___###",
|
|
135
|
+
getInstance: (dom, config, state) => new PciInstance(dom, config, state),
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const engineContext =
|
|
139
|
+
ctx && typeof (ctx as unknown as { register?: unknown }).register === "function"
|
|
140
|
+
? (ctx as unknown as { register: (f: IMSpciFactory<PropTypes>) => void })
|
|
141
|
+
: typeof window !== "undefined" && (window as unknown as { qtiCustomInteractionContext?: unknown }).qtiCustomInteractionContext
|
|
142
|
+
? ((window as unknown as { qtiCustomInteractionContext: { register: (f: IMSpciFactory<PropTypes>) => void } })
|
|
143
|
+
.qtiCustomInteractionContext as { register: (f: IMSpciFactory<PropTypes>) => void })
|
|
144
|
+
: undefined;
|
|
145
|
+
|
|
146
|
+
if (engineContext) {
|
|
147
|
+
engineContext.register(factory);
|
|
105
148
|
}
|
|
106
149
|
|
|
107
|
-
export default
|
|
150
|
+
export default factory;
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
ConfigProperties,
|
|
3
|
+
IMSpci,
|
|
4
|
+
IMSpciFactory,
|
|
5
|
+
QtiInteractionChangedDetail,
|
|
6
|
+
QtiVariableJSON,
|
|
7
|
+
} from "@citolab/tspci";
|
|
2
8
|
import * as ctx from "qtiCustomInteractionContext";
|
|
3
9
|
import configProps from "./config.json";
|
|
4
10
|
import Interaction from "./interaction"; // import and bundle this interaction file
|
|
@@ -8,94 +14,177 @@ import procenten from "./assets/procenten.png"; // image types are bundled insid
|
|
|
8
14
|
// Configuration
|
|
9
15
|
type PropTypes = typeof configProps;
|
|
10
16
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
dom: HTMLElement;
|
|
17
|
+
type PciState = {
|
|
18
|
+
value: string;
|
|
19
|
+
touched: boolean;
|
|
20
|
+
customValidity?: string;
|
|
21
|
+
};
|
|
17
22
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
23
|
+
class PciInstance implements IMSpci<PropTypes> {
|
|
24
|
+
typeIdentifier = "###___PCI_NAME___###";
|
|
25
|
+
private config: ConfigProperties<PropTypes>;
|
|
26
|
+
private dom: HTMLElement;
|
|
27
|
+
private shadowdom: ShadowRoot;
|
|
28
|
+
private state: PciState;
|
|
29
|
+
private onChange: (e: Event) => void;
|
|
30
|
+
|
|
31
|
+
constructor(dom: HTMLElement, config: ConfigProperties<PropTypes>, stateString?: string) {
|
|
32
|
+
if (!dom) throw new Error("No DOM Element provided");
|
|
33
|
+
config.properties = { ...configProps, ...config.properties };
|
|
34
|
+
this.dom = dom;
|
|
35
|
+
this.config = config;
|
|
36
|
+
this.shadowdom = dom.attachShadow({ mode: "closed" });
|
|
37
|
+
|
|
38
|
+
this.state = { value: "", touched: false };
|
|
39
|
+
if (stateString) {
|
|
40
|
+
try {
|
|
41
|
+
const parsed = JSON.parse(stateString) as Partial<PciState>;
|
|
42
|
+
this.state = {
|
|
43
|
+
value: typeof parsed.value === "string" ? parsed.value : "",
|
|
44
|
+
touched: parsed.touched === true,
|
|
45
|
+
customValidity: typeof parsed.customValidity === "string" ? parsed.customValidity : undefined,
|
|
46
|
+
};
|
|
47
|
+
} catch {
|
|
48
|
+
this.state = { value: typeof stateString === "string" ? stateString : "", touched: false };
|
|
49
|
+
}
|
|
21
50
|
}
|
|
22
|
-
}
|
|
23
51
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
throw new Error("No DOM Element provided");
|
|
52
|
+
const boundResponse =
|
|
53
|
+
this.config.boundTo?.[this.config.responseIdentifier] ??
|
|
54
|
+
(this.config.boundTo && Object.keys(this.config.boundTo).length === 1
|
|
55
|
+
? this.config.boundTo[Object.keys(this.config.boundTo)[0]]
|
|
56
|
+
: undefined);
|
|
57
|
+
if (boundResponse && (boundResponse.base || boundResponse.list || boundResponse.record)) {
|
|
58
|
+
this.setResponse?.(boundResponse);
|
|
32
59
|
}
|
|
33
|
-
|
|
34
|
-
this.
|
|
60
|
+
|
|
61
|
+
this.onChange = (e: Event) => {
|
|
62
|
+
const target = e.target as HTMLInputElement | null;
|
|
63
|
+
if (!target) return;
|
|
64
|
+
const nextValue = target.value ?? "";
|
|
65
|
+
if (nextValue === this.state.value && this.state.touched) return;
|
|
66
|
+
this.state = { ...this.state, value: nextValue, touched: true };
|
|
67
|
+
this.dispatchInteractionChanged();
|
|
68
|
+
};
|
|
35
69
|
|
|
36
70
|
this.render();
|
|
37
71
|
|
|
38
|
-
if (this.config.
|
|
39
|
-
|
|
40
|
-
const response = this.config.boundTo[responseIdentifier];
|
|
41
|
-
this.setResponse(response);
|
|
72
|
+
if (this.config.onready) {
|
|
73
|
+
this.config.onready(this, this.getState());
|
|
42
74
|
}
|
|
75
|
+
}
|
|
43
76
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
setResponse = (response: QtiVariableJSON) => {
|
|
48
|
-
// TODO restore the response
|
|
49
|
-
// Get the actual value by the variable type like:
|
|
50
|
-
// - response?.base?.string or response?.base?.integer
|
|
77
|
+
private getInputEl(): HTMLInputElement | null {
|
|
78
|
+
return this.shadowdom.querySelector("input[type='number']");
|
|
51
79
|
}
|
|
52
80
|
|
|
53
|
-
private
|
|
54
|
-
const
|
|
81
|
+
private dispatchInteractionChanged() {
|
|
82
|
+
const detail: QtiInteractionChangedDetail = {
|
|
55
83
|
interaction: this,
|
|
56
84
|
responseIdentifier: this.config.responseIdentifier,
|
|
57
|
-
valid: true,
|
|
85
|
+
valid: this.checkValidity?.() ?? true,
|
|
58
86
|
value: this.getResponse(),
|
|
59
87
|
};
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
88
|
+
const event = new CustomEvent("qti-interaction-changed", {
|
|
89
|
+
bubbles: true,
|
|
90
|
+
cancelable: true,
|
|
91
|
+
detail,
|
|
63
92
|
});
|
|
64
|
-
this.dom.dispatchEvent(
|
|
65
|
-
}
|
|
93
|
+
this.dom.dispatchEvent(event);
|
|
94
|
+
}
|
|
66
95
|
|
|
67
|
-
private render
|
|
68
|
-
this.shadowdom.addEventListener("change", (e: Event) => {
|
|
69
|
-
const value = (e.target as HTMLInputElement)?.value || "";
|
|
70
|
-
this.state = value;
|
|
71
|
-
this.interactionChanged();
|
|
72
|
-
});
|
|
96
|
+
private render() {
|
|
73
97
|
this.shadowdom.innerHTML = `<div class="pci-container">
|
|
74
98
|
<h1>${this.config.properties.title}</h1>
|
|
75
99
|
<div class="body">
|
|
76
|
-
<img width="${+this.config.properties.width}" height="${+this.config
|
|
100
|
+
<img width="${+this.config.properties.width}" height="${+this.config
|
|
101
|
+
.properties.height}" src="${procenten}" />
|
|
77
102
|
</div>
|
|
78
103
|
<div class="interaction">
|
|
79
|
-
<label
|
|
80
|
-
<input type="number" value="${this.state}" min="0" max="100">%
|
|
104
|
+
<label>${this.config.properties.prompt}</label>
|
|
105
|
+
<input type="number" value="${this.state.value}" min="0" max="100">%
|
|
81
106
|
</div>
|
|
82
107
|
</div>`;
|
|
108
|
+
|
|
83
109
|
const css = document.createElement("style");
|
|
84
110
|
css.innerHTML = style;
|
|
85
111
|
this.shadowdom.appendChild(css);
|
|
112
|
+
|
|
113
|
+
const input = this.getInputEl();
|
|
114
|
+
if (input) {
|
|
115
|
+
if (this.state.customValidity) input.setCustomValidity(this.state.customValidity);
|
|
116
|
+
input.addEventListener("input", this.onChange);
|
|
117
|
+
input.addEventListener("change", this.onChange);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
setResponse = (response: QtiVariableJSON) => {
|
|
122
|
+
const next =
|
|
123
|
+
typeof response?.base?.integer === "number"
|
|
124
|
+
? String(response.base.integer)
|
|
125
|
+
: typeof response?.base?.string === "string"
|
|
126
|
+
? response.base.string
|
|
127
|
+
: "";
|
|
128
|
+
this.state = { ...this.state, value: next, touched: true };
|
|
129
|
+
const input = this.getInputEl();
|
|
130
|
+
if (input) input.value = next;
|
|
86
131
|
};
|
|
87
132
|
|
|
88
133
|
getResponse = () => {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
return
|
|
134
|
+
if (!this.state.touched) return undefined;
|
|
135
|
+
const trimmed = (this.state.value ?? "").toString().trim();
|
|
136
|
+
if (!trimmed) return undefined;
|
|
137
|
+
const asNumber = Number(trimmed);
|
|
138
|
+
if (!Number.isFinite(asNumber)) return undefined;
|
|
139
|
+
return { base: { integer: Math.trunc(asNumber) } } as QtiVariableJSON;
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
getState = () => JSON.stringify(this.state);
|
|
143
|
+
|
|
144
|
+
checkValidity = () => {
|
|
145
|
+
const input = this.getInputEl();
|
|
146
|
+
if (!input) return undefined;
|
|
147
|
+
return input.checkValidity();
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
reportValidity = () => {
|
|
151
|
+
const input = this.getInputEl();
|
|
152
|
+
if (!input) return undefined;
|
|
153
|
+
return input.reportValidity();
|
|
94
154
|
};
|
|
95
155
|
|
|
96
|
-
|
|
156
|
+
setCustomValidity = (message: string) => {
|
|
157
|
+
this.state = { ...this.state, customValidity: message };
|
|
158
|
+
const input = this.getInputEl();
|
|
159
|
+
if (input) input.setCustomValidity(message);
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
getCustomValidity = () => this.state.customValidity ?? "";
|
|
163
|
+
|
|
164
|
+
oncompleted = () => {
|
|
165
|
+
const input = this.getInputEl();
|
|
166
|
+
if (input) {
|
|
167
|
+
input.removeEventListener("input", this.onChange);
|
|
168
|
+
input.removeEventListener("change", this.onChange);
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const factory: IMSpciFactory<PropTypes> = {
|
|
174
|
+
typeIdentifier: "###___PCI_NAME___###",
|
|
175
|
+
getInstance: (dom, config, state) => new PciInstance(dom, config, state),
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const engineContext =
|
|
179
|
+
ctx && typeof (ctx as unknown as { register?: unknown }).register === "function"
|
|
180
|
+
? (ctx as unknown as { register: (f: IMSpciFactory<PropTypes>) => void })
|
|
181
|
+
: typeof window !== "undefined" && (window as unknown as { qtiCustomInteractionContext?: unknown }).qtiCustomInteractionContext
|
|
182
|
+
? ((window as unknown as { qtiCustomInteractionContext: { register: (f: IMSpciFactory<PropTypes>) => void } })
|
|
183
|
+
.qtiCustomInteractionContext as { register: (f: IMSpciFactory<PropTypes>) => void })
|
|
184
|
+
: undefined;
|
|
97
185
|
|
|
98
|
-
|
|
186
|
+
if (engineContext) {
|
|
187
|
+
engineContext.register(factory);
|
|
99
188
|
}
|
|
100
189
|
|
|
101
|
-
export default
|
|
190
|
+
export default factory;
|
package/readme.md
CHANGED
|
@@ -97,38 +97,34 @@ Add the following PCI in the src folder `index.ts`
|
|
|
97
97
|
|
|
98
98
|
```ts
|
|
99
99
|
// src/index.ts
|
|
100
|
-
import { ConfigProperties, IMSpci } from "@citolab/tspci";
|
|
101
|
-
import * as ctx from "qtiCustomInteractionContext";
|
|
102
|
-
|
|
103
|
-
class
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
export default new Pci();
|
|
100
|
+
import { ConfigProperties, IMSpci, IMSpciFactory } from "@citolab/tspci";
|
|
101
|
+
import * as ctx from "qtiCustomInteractionContext";
|
|
102
|
+
|
|
103
|
+
class PciInstance implements IMSpci<{}> {
|
|
104
|
+
typeIdentifier = "HelloWorld"; // same as in package.json
|
|
105
|
+
private shadowdom: ShadowRoot;
|
|
106
|
+
|
|
107
|
+
constructor(dom: HTMLElement, config: ConfigProperties<{}>, state?: string) {
|
|
108
|
+
this.shadowdom = dom.attachShadow({ mode: "closed" });
|
|
109
|
+
this.render();
|
|
110
|
+
config.onready(this, this.getState());
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private render = () => {
|
|
114
|
+
this.shadowdom.innerHTML = `<div>Hello-World</div>`;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
getResponse = () => undefined;
|
|
118
|
+
getState = () => JSON.stringify({ v: 1 });
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const factory: IMSpciFactory<{}> = {
|
|
122
|
+
typeIdentifier: "HelloWorld",
|
|
123
|
+
getInstance: (dom, config, state) => new PciInstance(dom, config, state),
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
ctx && ctx.register(factory);
|
|
127
|
+
export default factory;
|
|
132
128
|
```
|
|
133
129
|
|
|
134
130
|
Add this in your root project: `global.d.ts` file
|
|
@@ -136,7 +132,7 @@ Add this in your root project: `global.d.ts` file
|
|
|
136
132
|
```ts
|
|
137
133
|
// global.d.ts
|
|
138
134
|
declare module "qtiCustomInteractionContext" {
|
|
139
|
-
const register: { register: (
|
|
135
|
+
const register: { register: (factory: { typeIdentifier: string; getInstance: Function }) => void };
|
|
140
136
|
export = register;
|
|
141
137
|
}
|
|
142
138
|
```
|
|
@@ -343,8 +339,9 @@ Also used in the TAO export for configuring PCIs in TAO
|
|
|
343
339
|
// add to types
|
|
344
340
|
+ private config: ConfigProperties<PropTypes>;
|
|
345
341
|
|
|
342
|
+
// in your factory.getInstance(...) (or instance constructor):
|
|
346
343
|
getInstance = (dom: HTMLElement, config: ConfigProperties<PropTypes>, stateString: string) => {
|
|
347
|
-
+ config.properties = { ...configProps, ...config.properties }; // merge our props with
|
|
344
|
+
+ config.properties = { ...configProps, ...config.properties }; // merge our props with the player's
|
|
348
345
|
+ this.config = config;
|
|
349
346
|
|
|
350
347
|
// destructure props for use in PCI
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// https://www.npmjs.com/package/rollup-plugin-live-server
|
|
4
4
|
// const ls = require("live-server");
|
|
5
5
|
|
|
6
|
-
import ls from "live-server";
|
|
6
|
+
import ls from "@compodoc/live-server";
|
|
7
7
|
|
|
8
8
|
export function liveServer(options = {}) {
|
|
9
9
|
const directories = options.directories || [];
|
package/types/index.d.ts
CHANGED
|
@@ -28,17 +28,6 @@ export interface ConfigProperties<T> {
|
|
|
28
28
|
export interface IMSpci<T> {
|
|
29
29
|
typeIdentifier: string;
|
|
30
30
|
|
|
31
|
-
/** @access public
|
|
32
|
-
* @method getInstance Create a new instance of this portable custom interaction
|
|
33
|
-
* Will be called by the qtiCustomInteractionContext
|
|
34
|
-
* @param {DOM Element} dom - the DOM Element this PCI is being added to
|
|
35
|
-
* @param {Object} configuration - the configuration to apply to this PCI
|
|
36
|
-
* @param {String} state - a previous saved state to apply to this instance.
|
|
37
|
-
* This must have been obtained from a prior call to getState on an
|
|
38
|
-
* instance of this type (same typeIdentifier)
|
|
39
|
-
*/
|
|
40
|
-
getInstance: (dom: HTMLElement, configuration: ConfigProperties<T>, state: string) => void;
|
|
41
|
-
|
|
42
31
|
/** @access public
|
|
43
32
|
* @method getResponse
|
|
44
33
|
* @return {Object} - the value to assign to the bound QTI response variable
|
|
@@ -52,8 +41,17 @@ export interface IMSpci<T> {
|
|
|
52
41
|
*/
|
|
53
42
|
getState: () => string;
|
|
54
43
|
oncompleted?: () => void;
|
|
55
|
-
|
|
56
|
-
|
|
44
|
+
checkValidity?: () => boolean | undefined;
|
|
45
|
+
reportValidity?: () => boolean | undefined;
|
|
46
|
+
setCustomValidity?: (message: string) => void;
|
|
47
|
+
getCustomValidity?: () => string;
|
|
48
|
+
setResponse?: (response: QtiVariableJSON) => void; // Not in the QTI 3.0 PCI instance interface, but used by some delivery engines/tools
|
|
49
|
+
destroy?: () => void; // Not used in QTI 3.0, but kept for compatibility
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface IMSpciFactory<T> {
|
|
53
|
+
typeIdentifier: string;
|
|
54
|
+
getInstance: (dom: HTMLElement, configuration: ConfigProperties<T>, state?: string) => IMSpci<T>;
|
|
57
55
|
}
|
|
58
56
|
|
|
59
57
|
export interface directedPair {
|