@citolab/tspci 1.5.7
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/package.json +69 -0
- package/public/index.html +351 -0
- package/readme.md +330 -0
- package/src/cli.js +130 -0
- package/src/rollup-plugin-live-server.js +32 -0
- package/src/rollup.config.dev.js +28 -0
- package/src/rollup.config.js +83 -0
- package/src/rollup.config.prod.js +10 -0
- package/tspci-development.png +0 -0
- package/types/index.d.ts +88 -0
package/package.json
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@citolab/tspci",
|
|
3
|
+
"version": "1.5.7",
|
|
4
|
+
"description": "an opiniated typescript ims-pci builder with tao integration",
|
|
5
|
+
"author": "patrick <patrick.deklein@cito.nl>",
|
|
6
|
+
"repository": "@citolab/tspci",
|
|
7
|
+
"bin": {
|
|
8
|
+
"tspci": "src/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"type": "module",
|
|
11
|
+
"types": "types/index.d.ts",
|
|
12
|
+
"devDependencies": {
|
|
13
|
+
"@rollup/plugin-alias": "^3.1.9",
|
|
14
|
+
"@rollup/plugin-babel": "^5.3.0",
|
|
15
|
+
"@rollup/plugin-commonjs": "^21.0.1",
|
|
16
|
+
"@rollup/plugin-image": "^2.1.1",
|
|
17
|
+
"@rollup/plugin-node-resolve": "13.1.3",
|
|
18
|
+
"@rollup/plugin-replace": "^3.0.1",
|
|
19
|
+
"@rollup/plugin-typescript": "^8.3.0",
|
|
20
|
+
"@rollup/plugin-json": "^4.1.0",
|
|
21
|
+
"@testing-library/dom": "^8.11.3",
|
|
22
|
+
"@typescript-eslint/eslint-plugin": "^5.11.0",
|
|
23
|
+
"@typescript-eslint/parser": "^5.11.0",
|
|
24
|
+
"rollup-plugin-watch-assets": "^1.0.1",
|
|
25
|
+
"postcss": "^8.4.6",
|
|
26
|
+
"postcss-import": "^14.0.2",
|
|
27
|
+
"rollup": "^2.67.2",
|
|
28
|
+
"rollup-plugin-postcss": "^4.0.2",
|
|
29
|
+
"rollup-plugin-string": "^3.0.0",
|
|
30
|
+
"rollup-plugin-terser": "^7.0.2",
|
|
31
|
+
"rollup-plugin-live-server": "^2.0.0",
|
|
32
|
+
"tailwindcss": "^3.0.21",
|
|
33
|
+
"live-server": "^1.2.1",
|
|
34
|
+
"rollup-plugin-copy": "^3.4.0",
|
|
35
|
+
"rollup-plugin-node-builtins": "^2.1.2",
|
|
36
|
+
"rollup-plugin-node-globals": "^1.4.0",
|
|
37
|
+
"@types/node": "^17.0.25"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@rollup/plugin-alias": "^3.1.9",
|
|
41
|
+
"@rollup/plugin-babel": "^5.3.0",
|
|
42
|
+
"@rollup/plugin-commonjs": "^21.0.1",
|
|
43
|
+
"@rollup/plugin-image": "^2.1.1",
|
|
44
|
+
"@rollup/plugin-node-resolve": "13.1.3",
|
|
45
|
+
"@rollup/plugin-replace": "^3.0.1",
|
|
46
|
+
"@rollup/plugin-typescript": "^8.3.0",
|
|
47
|
+
"@rollup/plugin-json": "^4.1.0",
|
|
48
|
+
"@testing-library/dom": "^8.11.3",
|
|
49
|
+
"@typescript-eslint/eslint-plugin": "^5.11.0",
|
|
50
|
+
"@typescript-eslint/parser": "^5.11.0",
|
|
51
|
+
"rollup-plugin-watch-assets": "^1.0.1",
|
|
52
|
+
"postcss": "^8.4.6",
|
|
53
|
+
"rollup": "^2.67.2",
|
|
54
|
+
"rollup-plugin-postcss": "^4.0.2",
|
|
55
|
+
"rollup-plugin-string": "^3.0.0",
|
|
56
|
+
"rollup-plugin-terser": "^7.0.2",
|
|
57
|
+
"rollup-plugin-live-server": "^2.0.0",
|
|
58
|
+
"tailwindcss": "^3.0.21",
|
|
59
|
+
"live-server": "^1.2.1",
|
|
60
|
+
"rollup-plugin-copy": "^3.4.0",
|
|
61
|
+
"rollup-plugin-node-builtins": "^2.1.2",
|
|
62
|
+
"rollup-plugin-node-globals": "^1.4.0",
|
|
63
|
+
"@types/node": "^17.0.25"
|
|
64
|
+
},
|
|
65
|
+
"scripts": {
|
|
66
|
+
"publish": "npm publish",
|
|
67
|
+
"publish:global": "npm publish --@citolab:registry=https://registry.npmjs.org/ --access public"
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<title>Hello RequireJS</title>
|
|
5
|
+
|
|
6
|
+
<link
|
|
7
|
+
href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0-beta1/dist/css/bootstrap.min.css"
|
|
8
|
+
rel="stylesheet"
|
|
9
|
+
integrity="sha384-0evHe/X+R7YkIZDRvuzKMRqM+OrBnVFBL6DOitfPri4tjfHxaWutUpFmBp4vmVor"
|
|
10
|
+
crossorigin="anonymous"
|
|
11
|
+
/>
|
|
12
|
+
|
|
13
|
+
<script
|
|
14
|
+
src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0-beta1/dist/js/bootstrap.bundle.min.js"
|
|
15
|
+
integrity="sha384-pprn3073KE6tl6bjs2QrFaJGz5/SUsLqktiwsUTF55Jfv3qYSDhgCecCxMW52nD2"
|
|
16
|
+
crossorigin="anonymous"
|
|
17
|
+
></script>
|
|
18
|
+
|
|
19
|
+
<script
|
|
20
|
+
src="https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.6/require.min.js"
|
|
21
|
+
integrity="sha512-c3Nl8+7g4LMSTdrm621y7kf9v3SDPnhxLNhcjFJbKECVnmZHTdo+IRO05sNLTH/D3vA6u1X32ehoLC7WFVdheg=="
|
|
22
|
+
crossorigin="anonymous"
|
|
23
|
+
referrerpolicy="no-referrer"
|
|
24
|
+
></script>
|
|
25
|
+
|
|
26
|
+
<style>
|
|
27
|
+
html {
|
|
28
|
+
font-size: 100%;
|
|
29
|
+
}
|
|
30
|
+
#pciContainer {
|
|
31
|
+
width: 100%;
|
|
32
|
+
/* min-width: 800px; */
|
|
33
|
+
max-width: 1024px;
|
|
34
|
+
height: 600px;
|
|
35
|
+
min-height: 600px;
|
|
36
|
+
/* max-height: 600px; */
|
|
37
|
+
border: 1px solid #eeeeee;
|
|
38
|
+
overflow: hidden;
|
|
39
|
+
margin: 0 auto;
|
|
40
|
+
background-color: white;
|
|
41
|
+
border-radius: 10px;
|
|
42
|
+
margin-top: 1rem;
|
|
43
|
+
}
|
|
44
|
+
#devtools {
|
|
45
|
+
max-width: 1024px;
|
|
46
|
+
margin: 0 auto;
|
|
47
|
+
}
|
|
48
|
+
</style>
|
|
49
|
+
</head>
|
|
50
|
+
<body class="bg-light">
|
|
51
|
+
<div class="container">
|
|
52
|
+
<div
|
|
53
|
+
class="alert alert-danger position-fixed mx-auto bottom-0"
|
|
54
|
+
style="display: none"
|
|
55
|
+
role="alert"
|
|
56
|
+
id="noresponse"
|
|
57
|
+
>
|
|
58
|
+
user has not yet interacted yet, getResponse should return undefined.
|
|
59
|
+
</div>
|
|
60
|
+
|
|
61
|
+
<div id="pciContainer" class="shadow-sm">
|
|
62
|
+
<div id="loading" class="text-muted mx-auto p-4">Loading...</div>
|
|
63
|
+
</div>
|
|
64
|
+
<div id="devtools"></div>
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
<script type="module">
|
|
68
|
+
import { h, Component, render } from "https://unpkg.com/preact@latest?module";
|
|
69
|
+
import { useState, useEffect } from "https://unpkg.com/preact@latest/hooks/dist/hooks.module.js?module";
|
|
70
|
+
import htm from "https://unpkg.com/htm?module";
|
|
71
|
+
// Initialize htm with Preact
|
|
72
|
+
const html = htm.bind(h);
|
|
73
|
+
|
|
74
|
+
function App({ item }) {
|
|
75
|
+
const [show, setShow] = useState(false);
|
|
76
|
+
const [response, setResponse] = useState(undefined);
|
|
77
|
+
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
localStorage.getItem(item.typeIdentifier + "_response");
|
|
80
|
+
}, []);
|
|
81
|
+
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
if (response !== undefined) {
|
|
84
|
+
localStorage.setItem(item.typeIdentifier + "_response", item.getResponse().base.string);
|
|
85
|
+
} else {
|
|
86
|
+
localStorage.removeItem(item.typeIdentifier + "_response");
|
|
87
|
+
item.resetResponse && item.resetResponse();
|
|
88
|
+
}
|
|
89
|
+
}, [response]);
|
|
90
|
+
|
|
91
|
+
let keyArray = [];
|
|
92
|
+
if (item.config && item.config.properties) {
|
|
93
|
+
keyArray = Object.keys(item.config.properties).map((keyName, keyIndex) => ({
|
|
94
|
+
key: keyName,
|
|
95
|
+
value: item.config.properties[keyName],
|
|
96
|
+
}));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function resetlocalstorage() {
|
|
100
|
+
localStorage.removeItem(item.typeIdentifier + "_state");
|
|
101
|
+
localStorage.removeItem(item.typeIdentifier + "_props");
|
|
102
|
+
setResponse(undefined);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function reset() {
|
|
106
|
+
resetlocalstorage();
|
|
107
|
+
location.reload();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function storeResponse() {
|
|
111
|
+
if (item.getResponse() !== undefined) {
|
|
112
|
+
setResponse(item.getResponse().base.string);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function compareResponse() {
|
|
117
|
+
const response = localStorage.getItem(item.typeIdentifier + "_response");
|
|
118
|
+
if (!response) {
|
|
119
|
+
alert("no response stored");
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (response === item.getResponse().base.string) {
|
|
123
|
+
alert("same");
|
|
124
|
+
} else {
|
|
125
|
+
alert("different");
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function resetResponse() {
|
|
130
|
+
setResponse(undefined);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function setStoredResponse() {
|
|
134
|
+
item.setResponse &&
|
|
135
|
+
item.setResponse({
|
|
136
|
+
base: {
|
|
137
|
+
string: response,
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function reload() {
|
|
143
|
+
const state = item.getState();
|
|
144
|
+
localStorage.setItem(item.typeIdentifier + "_state", JSON.stringify(state));
|
|
145
|
+
location.reload();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function changeConfig(key, value) {
|
|
149
|
+
item.config.properties[key] = value;
|
|
150
|
+
item.trigger(`change${key}`, value);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function reConfig(key, value) {
|
|
154
|
+
localStorage.setItem(item.typeIdentifier + "_props", JSON.stringify(item.config.properties));
|
|
155
|
+
location.reload();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function unload() {
|
|
159
|
+
item.destroy && item.destroy();
|
|
160
|
+
item.oncompleted && item.oncompleted();
|
|
161
|
+
var name = require.undef("index.js");
|
|
162
|
+
loadItem();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return html`
|
|
166
|
+
<div>
|
|
167
|
+
<div style="width:300px" class="shadow offcanvas offcanvas-end ${show ? "show visible" : ""}">
|
|
168
|
+
<div class="offcanvas-header">
|
|
169
|
+
<h5 class="offcanvas-title" id="offcanvasExampleLabel">properties</h5>
|
|
170
|
+
<button
|
|
171
|
+
type="button"
|
|
172
|
+
onClick="${() => setShow(false)}"
|
|
173
|
+
disabled="${!item.config}"
|
|
174
|
+
class="btn-close text-reset"
|
|
175
|
+
></button>
|
|
176
|
+
</div>
|
|
177
|
+
<div class="offcanvas-body">
|
|
178
|
+
<ul class="card-body">
|
|
179
|
+
${keyArray.map(
|
|
180
|
+
({ key, value }) => html`<div>
|
|
181
|
+
<div>${key}</div>
|
|
182
|
+
<input
|
|
183
|
+
type="text"
|
|
184
|
+
class="form-control"
|
|
185
|
+
value="${value}"
|
|
186
|
+
onkeyup="${(e) => changeConfig(key, e.target.value)}"
|
|
187
|
+
/>
|
|
188
|
+
</div>`
|
|
189
|
+
)}
|
|
190
|
+
</ul>
|
|
191
|
+
<button class="btn btn-primary" onClick="${() => reConfig()}">save and reload</button>
|
|
192
|
+
</div>
|
|
193
|
+
</div>
|
|
194
|
+
|
|
195
|
+
<div class="d-flex justify-content-start mt-3" role="toolbar" aria-label="Toolbar with button groups">
|
|
196
|
+
<div>
|
|
197
|
+
<small class="text-muted">configuration</small>
|
|
198
|
+
<div class="btn-group me-2 shadow-sm" role="group" aria-label="Basic checkbox toggle button group">
|
|
199
|
+
<button class="btn btn-outline-secondary" onClick="${() => setShow(true)}">props</button>
|
|
200
|
+
<button
|
|
201
|
+
class="btn btn-outline-secondary"
|
|
202
|
+
onClick="${() => alert(JSON.stringify(item.config), null, 4)}"
|
|
203
|
+
>
|
|
204
|
+
config
|
|
205
|
+
</button>
|
|
206
|
+
</div>
|
|
207
|
+
</div>
|
|
208
|
+
|
|
209
|
+
<div>
|
|
210
|
+
<small class="text-muted">data</small>
|
|
211
|
+
<div class="btn-group me-2 shadow-sm" role="group" aria-label="Basic checkbox toggle button group">
|
|
212
|
+
<button
|
|
213
|
+
class="btn btn-outline-secondary"
|
|
214
|
+
onClick="${() => alert(JSON.stringify(item.getResponse(), null, 4))}"
|
|
215
|
+
>
|
|
216
|
+
getResponse
|
|
217
|
+
</button>
|
|
218
|
+
<button class="btn btn-outline-secondary" onClick="${() => alert(item.getState())}">getState</button>
|
|
219
|
+
</div>
|
|
220
|
+
</div>
|
|
221
|
+
|
|
222
|
+
<div>
|
|
223
|
+
<small class="text-muted">state</small>
|
|
224
|
+
<div class="btn-group me-2 shadow-sm" role="group" aria-label="Basic checkbox toggle button group">
|
|
225
|
+
<button
|
|
226
|
+
class="btn btn-outline-secondary"
|
|
227
|
+
data-bs-placement="bottom"
|
|
228
|
+
data-bs-toggle="tooltip"
|
|
229
|
+
data-bs-html="true"
|
|
230
|
+
title="<ol class="m-0"><li class="text-start">state in localstorage</li><li>reload page</li><li>load state</li><li>remove localstorage</li></ol>"
|
|
231
|
+
onClick="${() => reload()}"
|
|
232
|
+
>
|
|
233
|
+
reload
|
|
234
|
+
</button>
|
|
235
|
+
</div>
|
|
236
|
+
</div>
|
|
237
|
+
|
|
238
|
+
<div>
|
|
239
|
+
<small class="text-muted">response</small>
|
|
240
|
+
<div class="btn-group me-2 shadow-sm" role="group" aria-label="Basic checkbox toggle button group">
|
|
241
|
+
<button class="btn btn-outline-secondary" data-bs-placement="bottom"
|
|
242
|
+
data-bs-toggle="tooltip"
|
|
243
|
+
data-bs-html="true"
|
|
244
|
+
title="Save response in localstorage" onClick="${() => storeResponse()}">store</button>
|
|
245
|
+
<button class="btn btn-outline-secondary" data-bs-placement="bottom"
|
|
246
|
+
data-bs-toggle="tooltip"
|
|
247
|
+
data-bs-html="true"
|
|
248
|
+
title="Compare current response with response in localstorage" onClick="${() =>
|
|
249
|
+
compareResponse()}">compare</button>
|
|
250
|
+
<button class="btn btn-outline-secondary" data-bs-placement="bottom"
|
|
251
|
+
data-bs-toggle="tooltip"
|
|
252
|
+
data-bs-html="true"
|
|
253
|
+
title="set stored response back into item with setResponse (*TAO PCI only)" disabled="${!item.setResponse}" onClick="${() =>
|
|
254
|
+
setStoredResponse()}">set</button>
|
|
255
|
+
<button class="btn btn-outline-secondary" data-bs-placement="bottom"
|
|
256
|
+
data-bs-toggle="tooltip"
|
|
257
|
+
data-bs-html="true"
|
|
258
|
+
title="reset reponse in item (*TAO PCI only)" disabled="${!item.resetResponse}" onClick="${() =>
|
|
259
|
+
resetResponse()}">reset</button>
|
|
260
|
+
</div>
|
|
261
|
+
<div>${response}</div>
|
|
262
|
+
</div>
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
<div>
|
|
268
|
+
<small class="text-muted">ㅤ</small>
|
|
269
|
+
<div class="d-flex">
|
|
270
|
+
<button data-bs-toggle="tooltip"
|
|
271
|
+
disabled="${!(item.destroy || item.oncompleted)}"
|
|
272
|
+
data-bs-html="true"
|
|
273
|
+
title="reset everything in localstorage (config/state/response) and reload" class="btn bg-light shadow-sm btn-outline-danger me-2" onClick="${() => unload()}">
|
|
274
|
+
unload
|
|
275
|
+
</button>
|
|
276
|
+
<button data-bs-toggle="tooltip"
|
|
277
|
+
data-bs-html="true"
|
|
278
|
+
title="reset everything in localstorage (config/state/response) and reload" class="btn bg-light shadow-sm btn-outline-danger" onClick="${() => reset()}">reset</button>
|
|
279
|
+
</div>
|
|
280
|
+
</div>
|
|
281
|
+
</div>
|
|
282
|
+
</div>
|
|
283
|
+
`;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function register(item) {
|
|
287
|
+
let state = undefined;
|
|
288
|
+
const localStorageState = localStorage.getItem(item.typeIdentifier + "_state");
|
|
289
|
+
if (localStorageState) {
|
|
290
|
+
state = JSON.parse(localStorageState);
|
|
291
|
+
}
|
|
292
|
+
let props = {};
|
|
293
|
+
const localStorageProps = localStorage.getItem(item.typeIdentifier + "_props");
|
|
294
|
+
if (localStorageProps) {
|
|
295
|
+
props = JSON.parse(localStorageProps);
|
|
296
|
+
}
|
|
297
|
+
const dom = document.getElementById("pciContainer");
|
|
298
|
+
const config = {
|
|
299
|
+
onready: () => {
|
|
300
|
+
document.getElementById("loading").innerHTML = "";
|
|
301
|
+
},
|
|
302
|
+
properties: props,
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
item.getInstance(dom, config, state);
|
|
306
|
+
|
|
307
|
+
if (item.getResponse() !== undefined) {
|
|
308
|
+
document.getElementById("noresponse").style.display = "block";
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
localStorage.removeItem(item.typeIdentifier + "_state");
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
define("qtiCustomInteractionContext", (qtiCustomInteractionContext) => {
|
|
315
|
+
return { register: (ctxA) => register(ctxA) };
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
function loadItem() {
|
|
319
|
+
requirejs(["index.js"], (itemInstance) => {
|
|
320
|
+
render(html`<${App} item="${itemInstance}" />`, document.getElementById("devtools"));
|
|
321
|
+
// enable tooltips
|
|
322
|
+
var tooltipList = [].slice
|
|
323
|
+
.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'))
|
|
324
|
+
.map(function (tooltipTriggerEl) {
|
|
325
|
+
return new bootstrap.Tooltip(tooltipTriggerEl);
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
window.onload = () => loadItem();
|
|
330
|
+
</script>
|
|
331
|
+
|
|
332
|
+
<h3 style="margin-top: 12rem">checklist</h3>
|
|
333
|
+
<ul style="margin-bottom: 10rem">
|
|
334
|
+
<li>Is the css scoped to the item?</li>
|
|
335
|
+
<li>Is the css/item somewhat responsive on smaller screens</li>
|
|
336
|
+
<li>Can you reset the item to its initial state</li>
|
|
337
|
+
<li>Can it restore its state ( reload )</li>
|
|
338
|
+
<li>Can you change the config</li>
|
|
339
|
+
<li>Colorblind</li>
|
|
340
|
+
<li>Touch/Mouse friendly</li>
|
|
341
|
+
<li>Works in older browser like IE11</li>
|
|
342
|
+
<li>Destroys itself correctly, removing event listeners</li>
|
|
343
|
+
<li>No objects or variables on global scope</li>
|
|
344
|
+
<li>Triggers correctly on changed config properties</li>
|
|
345
|
+
<li>Implements on off() methods of tao?</li>
|
|
346
|
+
<li>Response type and response correct</li>
|
|
347
|
+
<li>getResponse() should give undefined when user has not done anything</li>
|
|
348
|
+
<li>For TAO to enter response is the entered comparable by the response given</li>
|
|
349
|
+
</ul>
|
|
350
|
+
</body>
|
|
351
|
+
</html>
|
package/readme.md
ADDED
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
<h1>
|
|
2
|
+
A modern QTI-PCI development environment creating and testing portable custom interactions
|
|
3
|
+
</h1>
|
|
4
|
+
|
|
5
|
+
<p align="center">
|
|
6
|
+
<img src="tspci-development.png" alt="Typescript Pci Builder">
|
|
7
|
+
</p>
|
|
8
|
+
|
|
9
|
+
## ✨ Features <a name="features"></a>
|
|
10
|
+
|
|
11
|
+
- Use modern tooling like typescript, preact, postcss and tailwind
|
|
12
|
+
- Build and bundle your library PCI using only a `package.json`
|
|
13
|
+
- Test and develop all your PCI states, configs, responses and lifecycle without uploading
|
|
14
|
+
- Develop in live server which directly updates on code changes
|
|
15
|
+
- Produces a single bundle .js file to use in any delivery engine that supports the latest IMS PCI standard
|
|
16
|
+
- 0 configuration TypeScript support
|
|
17
|
+
- Bundles all your css, images and json files.
|
|
18
|
+
- Postcss support, use CSS nesting out of the box.
|
|
19
|
+
- First class (p)react and tailwind support
|
|
20
|
+
- Extension for TAO, configuring icon, label, support for setting correct response and pci-properties
|
|
21
|
+
|
|
22
|
+
## 🔧 Installation & Setup
|
|
23
|
+
|
|
24
|
+
1️⃣ **Install** by running: `npm i -D @citolab/tspci`
|
|
25
|
+
|
|
26
|
+
2️⃣ **Set up** your `package.json`:
|
|
27
|
+
|
|
28
|
+
```json
|
|
29
|
+
// package.json
|
|
30
|
+
{
|
|
31
|
+
"name": "@citolab/hello-world",
|
|
32
|
+
"description": "Hello world pci",
|
|
33
|
+
"version": "1.0.0",
|
|
34
|
+
"main": "dist/index.js",
|
|
35
|
+
"types": "dist/types.d.ts",
|
|
36
|
+
"dependencies": {},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@citolab/tspci": "^1.5.6",
|
|
39
|
+
"@citolab/tspci-tao": "^1.0.0"
|
|
40
|
+
},
|
|
41
|
+
"scripts": {
|
|
42
|
+
"dev": "tspci --dev",
|
|
43
|
+
"prod": "tspci"
|
|
44
|
+
},
|
|
45
|
+
"source": "src/index.ts"
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Add the following PCI in the src folder `index.ts`
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
// src/index.ts
|
|
53
|
+
import { Configuration, IMSpci } from "@citolab/tspci";
|
|
54
|
+
import * as ctx from "qtiCustomInteractionContext";
|
|
55
|
+
|
|
56
|
+
class Pci implements IMSpci<{}> {
|
|
57
|
+
typeIdentifier = "HelloWorld"; // same as in package.json
|
|
58
|
+
shadowdom: ShadowRoot;
|
|
59
|
+
|
|
60
|
+
constructor() {
|
|
61
|
+
ctx && ctx.register(this);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
getInstance = (dom: HTMLElement, config: Configuration<any>, state: string) => {
|
|
65
|
+
this.shadowdom = dom.attachShadow({ mode: "closed" });
|
|
66
|
+
this.render();
|
|
67
|
+
config.onready(this);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
private render = () => {
|
|
71
|
+
this.shadowdom.innerHTML = `<div>Hello-World</div>`;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
getResponse = () => {
|
|
75
|
+
return null;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
getState = () => null;
|
|
79
|
+
}
|
|
80
|
+
export default new Pci();
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Add this in your root project: `global.d.ts` file
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
// global.d.ts
|
|
87
|
+
declare module "qtiCustomInteractionContext" {
|
|
88
|
+
const register: { register: (PortableInteraction) => void };
|
|
89
|
+
export = register;
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Add a `tsconfig.json` to your project for type checking
|
|
94
|
+
|
|
95
|
+
```json
|
|
96
|
+
{
|
|
97
|
+
"compilerOptions": {
|
|
98
|
+
"declaration": true,
|
|
99
|
+
"lib": ["es6", "dom", "dom.iterable"],
|
|
100
|
+
"module": "es6",
|
|
101
|
+
"moduleResolution": "node",
|
|
102
|
+
"removeComments": true,
|
|
103
|
+
"sourceMap": true,
|
|
104
|
+
"strict": false,
|
|
105
|
+
"target": "es6",
|
|
106
|
+
"outDir": "./dist",
|
|
107
|
+
"jsx": "react-jsx",
|
|
108
|
+
"jsxImportSource": "preact",
|
|
109
|
+
"allowSyntheticDefaultImports": true,
|
|
110
|
+
"resolveJsonModule": true,
|
|
111
|
+
"esModuleInterop": true,
|
|
112
|
+
},
|
|
113
|
+
"include": ["**/*"],
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
3️⃣ **Try it out** by running `tspci --dev`.
|
|
118
|
+
|
|
119
|
+
### All CLI Options
|
|
120
|
+
|
|
121
|
+
```
|
|
122
|
+
Usage
|
|
123
|
+
$ tspci <command> [options]
|
|
124
|
+
|
|
125
|
+
Available Commands
|
|
126
|
+
--dev -d Start development server
|
|
127
|
+
--watch -w Only watch changes
|
|
128
|
+
--help -h Help about commandos
|
|
129
|
+
--target Build production for platform
|
|
130
|
+
|
|
131
|
+
Examples package.json scripts
|
|
132
|
+
$ "dev": "tspci --dev",
|
|
133
|
+
$ "prod": "tspci",
|
|
134
|
+
$ "watch": "tspci --watch",
|
|
135
|
+
$ "package-tao": "tspci --target tao"
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## 🛣 What's next
|
|
139
|
+
|
|
140
|
+
Use one of the [examples](https://github.com/Citolab/tspci-helloworld) to get a headstart
|
|
141
|
+
|
|
142
|
+
Or read further how we build our PCIs
|
|
143
|
+
|
|
144
|
+
Our prefered layout of our PCIs
|
|
145
|
+
```
|
|
146
|
+
your-pci-project
|
|
147
|
+
│ package.json // Definition of your PCI in a standard package.json, TAO adds some props
|
|
148
|
+
│ global.d.ts // Tell typescript qtiCustomInteractionContext is supplied later, adds a shim
|
|
149
|
+
│ tsconfig.json // Typescript configuration, also for type checking your code
|
|
150
|
+
│
|
|
151
|
+
└───src
|
|
152
|
+
│ │ config.json // Used to configure PCI, and in TAO for authors
|
|
153
|
+
│ │ index.ts // The api for the player and bootstraps your interaction
|
|
154
|
+
│ │ interaction.ts // the actual interaction, preferably a main preact component
|
|
155
|
+
│ │ store.ts // definition of the state and all possible state mutations of your pci
|
|
156
|
+
│ └───style.css // css styles, imported by your pci and possible to use tailwind classes
|
|
157
|
+
│
|
|
158
|
+
│ postcss.config.js // optionally adds postcss processing of css
|
|
159
|
+
│ tailwind.config.js // include tailwind classes
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
### Use JSX
|
|
164
|
+
Use (p)react in your PCI's to create interactions and bind your data to HTML
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
+ import { h, render } from "preact";
|
|
168
|
+
|
|
169
|
+
render(null, this.shadowdom);
|
|
170
|
+
|
|
171
|
+
render = () => {
|
|
172
|
+
+ render(null, this.shadowdom);
|
|
173
|
+
+ render(<Interaction config={this.config.properties} />, this.shadowdom);
|
|
174
|
+
};
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### Bundle your css
|
|
178
|
+
Import and bundle css
|
|
179
|
+
Appending css to shadowdom will prevent styles leaking into your player
|
|
180
|
+
|
|
181
|
+
```ts
|
|
182
|
+
+ import style from "./style.css";
|
|
183
|
+
|
|
184
|
+
render = () => {
|
|
185
|
+
render(...
|
|
186
|
+
+ const css = document.createElement("style");
|
|
187
|
+
+ css.innerHTML = style;
|
|
188
|
+
+ this.shadowdom.appendChild(css);
|
|
189
|
+
render(...
|
|
190
|
+
};
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### Use modern css
|
|
194
|
+
add `postcss.config.js` with autoprefixer
|
|
195
|
+
|
|
196
|
+
```json
|
|
197
|
+
// postcss.config.js
|
|
198
|
+
module.exports = {
|
|
199
|
+
plugins: [
|
|
200
|
+
require('postcss-import'),
|
|
201
|
+
require('autoprefixer'),
|
|
202
|
+
]
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### Modern UI with existing classes
|
|
207
|
+
|
|
208
|
+
add tailwind to `postcss.config.js`
|
|
209
|
+
and add a `tailwind.config.js`
|
|
210
|
+
|
|
211
|
+
```js
|
|
212
|
+
// postcss.config.js
|
|
213
|
+
plugins: [
|
|
214
|
+
+ require('tailwindcss/nesting'),
|
|
215
|
+
+ require('tailwindcss'),
|
|
216
|
+
];
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
```js
|
|
220
|
+
// tailwind.config.js
|
|
221
|
+
module.exports = {
|
|
222
|
+
content: ["./src/**/*.{html,js,ts,jsx,tsx}", "./*.xml"], // html/javascript and typescript, and everything in pci markup
|
|
223
|
+
theme: {
|
|
224
|
+
extend: {},
|
|
225
|
+
},
|
|
226
|
+
plugins: [],
|
|
227
|
+
};
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### Bundle all your images
|
|
231
|
+
Images are bundled in the js by importing them in code:
|
|
232
|
+
imports JPG, PNG, GIF, SVG, and WebP files
|
|
233
|
+
|
|
234
|
+
```ts
|
|
235
|
+
import procenten from "./assets/procenten.png";
|
|
236
|
+
<img src={procenten} />;
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### Create advanced PCIs with 3D libraries
|
|
240
|
+
For example, import threejs to create 3D pci's
|
|
241
|
+
|
|
242
|
+
```ts
|
|
243
|
+
// src/interaction.ts
|
|
244
|
+
import * as THREE from "three";
|
|
245
|
+
|
|
246
|
+
class VoxelPainterClass {
|
|
247
|
+
private scene: THREE.Scene;
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### Record user interaction
|
|
251
|
+
A store will centralize your PCI state but also all mutations.
|
|
252
|
+
This means, this way you could replay everything a user did
|
|
253
|
+
And you can use Redux devtools to debug
|
|
254
|
+
|
|
255
|
+
```ts
|
|
256
|
+
// src/index.ts
|
|
257
|
+
const [state, dispatch] = useStore<StateModel, ActionType>((type, payload) => {});
|
|
258
|
+
|
|
259
|
+
dispatch<{ x: number }>("ADD_ACTION", { x: +inputValue });
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### Let authors configure your PCI in QTI
|
|
263
|
+
Use properties in config.json to let authors configure your PCI
|
|
264
|
+
Also used in the TAO export for configuring PCIs in TAO
|
|
265
|
+
```json
|
|
266
|
+
// src/config.json
|
|
267
|
+
{
|
|
268
|
+
"buttonText": "Calculate",
|
|
269
|
+
"sum1" : "$1 * 12 + 3",
|
|
270
|
+
"sum2" : "$1 * 4 + 53",
|
|
271
|
+
"tableSize" : "4"
|
|
272
|
+
}
|
|
273
|
+
```
|
|
274
|
+
```ts
|
|
275
|
+
// src/index.ts
|
|
276
|
+
+ import configProps from "./config.json";
|
|
277
|
+
+ type PropTypes = typeof configProps;
|
|
278
|
+
|
|
279
|
+
// add to types
|
|
280
|
+
+ private config: Configuration<PropTypes>;
|
|
281
|
+
|
|
282
|
+
getInstance = (dom: HTMLElement, config: Configuration<PropTypes>, stateString: string) => {
|
|
283
|
+
+ config.properties = { ...configProps, ...config.properties }; // merge our props with players
|
|
284
|
+
+ this.config = config;
|
|
285
|
+
|
|
286
|
+
// destructure props for use in PCI
|
|
287
|
+
+ const { sum1, sum2, buttonText, tableSize } = config;
|
|
288
|
+
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
### Make use of existing react hooks
|
|
292
|
+
```ts
|
|
293
|
+
// src/useMousePosition
|
|
294
|
+
import { useEffect, useState } from "react";
|
|
295
|
+
|
|
296
|
+
export const useMousePosition = () => {
|
|
297
|
+
const [position, setPosition] = useState({ x: 0, y: 0 });
|
|
298
|
+
|
|
299
|
+
useEffect(() => {
|
|
300
|
+
const setPosFromEvent = (e) => setPosition({ x: e.clientX, y: e.clientY });
|
|
301
|
+
window.addEventListener("mousemove", setPosFromEvent);
|
|
302
|
+
return () => window.removeEventListener("mousemove", setPosFromEvent);
|
|
303
|
+
}, []);
|
|
304
|
+
|
|
305
|
+
return { clientX: position.x, clientY: position.y }
|
|
306
|
+
};
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
### Use in TAO
|
|
310
|
+
Tao adds some lifecycle methods which you can implement and we supply an extended interfaee on top of the IMS one.
|
|
311
|
+
|
|
312
|
+
More info, see our extension
|
|
313
|
+
[@citolab/tspci-tao](https://github.com/Citolab/qti-pci-bundler/tree/main/lib/tspci-tao)
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
## Roadmap
|
|
317
|
+
|
|
318
|
+
- Packager for Facet
|
|
319
|
+
- Packager for Trifork
|
|
320
|
+
- Create PCI with Vue
|
|
321
|
+
- Create PCI with Haunted
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
If you want your platform to be support, [contact us](mailto://getinspiredbycitolab@gmail.com)
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
## License
|
|
328
|
+
|
|
329
|
+
[GPL3](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
|
330
|
+
[rollup]: https://github.com/rollup/rollup
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import loadConfigFile from "rollup/dist/loadConfigFile.js";
|
|
3
|
+
import { rollup, watch } from "rollup";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import fs from "fs";
|
|
6
|
+
|
|
7
|
+
const args = process.argv.slice(2);
|
|
8
|
+
|
|
9
|
+
// PK: retreive dirname, most of the time automaticly inserted in es modules
|
|
10
|
+
import { fileURLToPath } from "url";
|
|
11
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
12
|
+
const __dirname = path.dirname(__filename);
|
|
13
|
+
|
|
14
|
+
const PACKAGE_ROOT_PATH = process.cwd();
|
|
15
|
+
const PACKAGE_DIST_PATH = path.join(PACKAGE_ROOT_PATH, "dist");
|
|
16
|
+
const NODE_MODULES = path.join(process.cwd(), "node_modules");
|
|
17
|
+
|
|
18
|
+
// https://rollupjs.org/guide/en/#programmatically-loading-a-config-file
|
|
19
|
+
const watchBuild = (devServer = false) => {
|
|
20
|
+
const rollupFile = devServer ? "rollup.config.dev.js" : "rollup.config.js";
|
|
21
|
+
|
|
22
|
+
fs.rmSync(PACKAGE_DIST_PATH, { recursive: true, force: true });
|
|
23
|
+
!fs.existsSync(PACKAGE_DIST_PATH) && fs.mkdirSync(PACKAGE_DIST_PATH);
|
|
24
|
+
|
|
25
|
+
if (devServer) {
|
|
26
|
+
try {
|
|
27
|
+
if (fs.existsSync(path.join(PACKAGE_ROOT_PATH, "src/index.html"))) {
|
|
28
|
+
fs.copyFile(
|
|
29
|
+
path.join(PACKAGE_ROOT_PATH, "src/index.html"),
|
|
30
|
+
path.join(PACKAGE_DIST_PATH, "index.html"),
|
|
31
|
+
(err) => {
|
|
32
|
+
if (err) throw err;
|
|
33
|
+
console.log("using index.html found in src/index.html, copied to dist");
|
|
34
|
+
}
|
|
35
|
+
);
|
|
36
|
+
} else {
|
|
37
|
+
fs.copyFile(path.join(__dirname, "../public/index.html"), path.join(PACKAGE_DIST_PATH, "index.html"), (err) => {
|
|
38
|
+
if (err) throw err;
|
|
39
|
+
console.log("using index.html was copied to /dist folder pci");
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
} catch (err) {
|
|
43
|
+
console.error(err);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
loadConfigFile(path.resolve(__dirname, rollupFile), {}).then(async ({ options, warnings }) => {
|
|
47
|
+
console.log(`We currently have ${warnings.count} warnings`);
|
|
48
|
+
warnings.flush();
|
|
49
|
+
const watcher = watch(options);
|
|
50
|
+
watcher.on("event", (event) => {
|
|
51
|
+
event.code == "START" && console.log("starting build");
|
|
52
|
+
event.code == "ERROR" && console.log("ERROR", event);
|
|
53
|
+
});
|
|
54
|
+
watcher.on("event", ({ result }) => {
|
|
55
|
+
if (result) {
|
|
56
|
+
result.close();
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
watcher.close();
|
|
60
|
+
});
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const prodBuild = async () => {
|
|
64
|
+
fs.rmSync(path.join(PACKAGE_ROOT_PATH, "dist"), { recursive: true, force: true });
|
|
65
|
+
return loadConfigFile(path.resolve(__dirname, "rollup.config.prod.js"), {}).then(async ({ options, warnings }) => {
|
|
66
|
+
console.log(`We currently have ${warnings.count} warnings`);
|
|
67
|
+
warnings.flush();
|
|
68
|
+
for (const optionsObj of options) {
|
|
69
|
+
const bundle = await rollup(optionsObj);
|
|
70
|
+
await Promise.all(optionsObj.output.map(bundle.write));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// if supplying your own index.html, this should be copied to the dist folder
|
|
74
|
+
try {
|
|
75
|
+
if (fs.existsSync(path.join(PACKAGE_ROOT_PATH, "src/index.html"))) {
|
|
76
|
+
fs.copyFile(
|
|
77
|
+
path.join(PACKAGE_ROOT_PATH, "src/index.html"),
|
|
78
|
+
path.join(PACKAGE_DIST_PATH, "index.html"),
|
|
79
|
+
(err) => {
|
|
80
|
+
if (err) throw err;
|
|
81
|
+
console.log(`using ${PACKAGE_ROOT_PATH}, "src/index.html", copied to ${PACKAGE_DIST_PATH}`);
|
|
82
|
+
}
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
} catch (err) {
|
|
86
|
+
console.error(err);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const run = async () => {
|
|
92
|
+
const help = args.indexOf("--help") >= 0 || args.indexOf("-h") >= 0;
|
|
93
|
+
const watch = args.indexOf("--watch") >= 0 || args.indexOf("-w") >= 0;
|
|
94
|
+
const dev = args.indexOf("--dev") >= 0 || args.indexOf("-d") >= 0;;
|
|
95
|
+
const target = args.indexOf("-target") >= 0;
|
|
96
|
+
|
|
97
|
+
if (help) {
|
|
98
|
+
console.log("--help -h list these commands");
|
|
99
|
+
console.log("--build a one of build of your item");
|
|
100
|
+
console.log("--watch watch the files");
|
|
101
|
+
console.log("--target build a prod item and bundle for platform");
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (watch) {
|
|
106
|
+
watchBuild(false);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (dev) {
|
|
111
|
+
watchBuild(true);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
prodBuild().then(() => {
|
|
116
|
+
if (target) {
|
|
117
|
+
const targetIndex = args.indexOf("--target");
|
|
118
|
+
if (args.length <= targetIndex + 1) {
|
|
119
|
+
console.error("Missing target name. Use tspci --target target");
|
|
120
|
+
}
|
|
121
|
+
const targetname = args[targetIndex + 1];
|
|
122
|
+
const scriptFile = path.join(NODE_MODULES, `@citolab\\tspci-${targetname}\\src\\createPackage.mjs`);
|
|
123
|
+
import(`file:///${scriptFile}`).then((script) => {
|
|
124
|
+
script.bundle();
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
run();
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// PK: made a pull request for this https://bitbucket.org/robhicks55/rollup-plugin-live-server/pull-requests/
|
|
2
|
+
// WATCH argument added to live server, else this package could just be used
|
|
3
|
+
// https://www.npmjs.com/package/rollup-plugin-live-server
|
|
4
|
+
const ls = require("live-server");
|
|
5
|
+
|
|
6
|
+
export function liveServer(options = {}) {
|
|
7
|
+
const directories = options.directories || [];
|
|
8
|
+
const params = {
|
|
9
|
+
file: options.file || 'index.html',
|
|
10
|
+
host: options.host || '0.0.0.0',
|
|
11
|
+
logLevel: options.logLevel || 2,
|
|
12
|
+
open: options.open || false,
|
|
13
|
+
port: options.port ||8080,
|
|
14
|
+
root: options.root || '.',
|
|
15
|
+
wait: options.wait || 200,
|
|
16
|
+
watch: options.watch || "",
|
|
17
|
+
};
|
|
18
|
+
if (options.mount) params.mount = options.mount;
|
|
19
|
+
if (options.ignore) params.ignore = options.ignore;
|
|
20
|
+
if (options.middleware) params.middleware = options.middleware;
|
|
21
|
+
if (options.cert) params.cert = options.cert;
|
|
22
|
+
if (options.key) params.key = options.key;
|
|
23
|
+
if (options.passphrase) params.passphrase = options.passphrase;
|
|
24
|
+
|
|
25
|
+
ls.start(params);
|
|
26
|
+
return {
|
|
27
|
+
name: 'liveServer',
|
|
28
|
+
generateBundle() {
|
|
29
|
+
console.log(`live-server running on ${params.port}`);
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import watchAssets from 'rollup-plugin-watch-assets';
|
|
2
|
+
// PK: not the official npm package, but a copy, needed watch functinoality
|
|
3
|
+
import { liveServer } from "./rollup-plugin-live-server";
|
|
4
|
+
|
|
5
|
+
import path from "path";
|
|
6
|
+
|
|
7
|
+
const PACKAGE_ROOT_PATH = process.cwd()
|
|
8
|
+
const PACKAGE_DIST_PATH = path.join(PACKAGE_ROOT_PATH, "dist")
|
|
9
|
+
|
|
10
|
+
const livesrvr = liveServer({
|
|
11
|
+
root: path.join(PACKAGE_ROOT_PATH),
|
|
12
|
+
mount: [["/", "./dist"]],
|
|
13
|
+
open: true,
|
|
14
|
+
wait: 500,
|
|
15
|
+
watch: PACKAGE_DIST_PATH
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const watchassets = watchAssets({ assets: ['./src/**/*.json', './src/**/*.svg', './src/**/*.png'] })
|
|
19
|
+
|
|
20
|
+
import rollupConfig from "./rollup.config";
|
|
21
|
+
|
|
22
|
+
const newRollupConfig = {
|
|
23
|
+
...rollupConfig,
|
|
24
|
+
output: rollupConfig.output.map((output) => ({ ...output, sourcemap: true })),
|
|
25
|
+
plugins: [...rollupConfig.plugins, livesrvr, watchassets],
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export default newRollupConfig;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import resolve from "@rollup/plugin-node-resolve";
|
|
2
|
+
import typescript from "@rollup/plugin-typescript";
|
|
3
|
+
import alias from "@rollup/plugin-alias";
|
|
4
|
+
import image from "@rollup/plugin-image";
|
|
5
|
+
import replace from "@rollup/plugin-replace";
|
|
6
|
+
import postcss from "rollup-plugin-postcss";
|
|
7
|
+
import commonjs from "@rollup/plugin-commonjs"; // added this when added react-redux, gave an ' 'default' is not exported by node_modules/prop-types/index.js'
|
|
8
|
+
import json from "@rollup/plugin-json";
|
|
9
|
+
|
|
10
|
+
// copy, builtins, globals and litTailwind were added for use in simple bundle tool to make qti players
|
|
11
|
+
import copy from "rollup-plugin-copy"; // copying assets to the assets folder in dist ( not for PCI, those assets are bundled )
|
|
12
|
+
import builtins from 'rollup-plugin-node-builtins'; // was fix for including pixi for QTI component development, see: https://github.com/rollup/rollup/issues/3230#issuecomment-585416594
|
|
13
|
+
import globals from 'rollup-plugin-node-globals'; // was fix for including pixi for QTI component development, see: https://github.com/rollup/rollup/issues/3230#issuecomment-585416594
|
|
14
|
+
// import litTailwind from "rollup-plugin-lit-tailwindcss3"; // Qti development, custom components can include tailwind classes
|
|
15
|
+
|
|
16
|
+
import fs from "fs";
|
|
17
|
+
import path from "path";
|
|
18
|
+
|
|
19
|
+
const PACKAGE_ROOT_PATH = process.cwd();
|
|
20
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(PACKAGE_ROOT_PATH, "package.json"), "utf-8"));
|
|
21
|
+
|
|
22
|
+
export default {
|
|
23
|
+
input: [`${pkg.source}`],
|
|
24
|
+
external: ["qtiCustomInteractionContext"],
|
|
25
|
+
watch: {
|
|
26
|
+
include: "./src/**", // fixme: watch these files, does not support watching the css
|
|
27
|
+
clearScreen: false, // clears the bash log before running
|
|
28
|
+
},
|
|
29
|
+
output: [
|
|
30
|
+
{
|
|
31
|
+
name: `${pkg.name}`,
|
|
32
|
+
file: `${pkg.main}`,
|
|
33
|
+
format: "umd",
|
|
34
|
+
sourcemap: true,
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
plugins: [
|
|
38
|
+
globals(),
|
|
39
|
+
builtins(),
|
|
40
|
+
alias({
|
|
41
|
+
// When using third party react libraries, alias them to preact
|
|
42
|
+
entries: [
|
|
43
|
+
{ find: "qtiCustomInteractionContext", replacement: "./qtiShim" },
|
|
44
|
+
{ find: "react", replacement: "preact/compat" },
|
|
45
|
+
{ find: "react-dom/test-utils", replacement: "preact/test-utils" },
|
|
46
|
+
{ find: "react-dom", replacement: "preact/compat" },
|
|
47
|
+
{ find: "react/jsx-runtime", replacement: "preact/jsx-runtime" },
|
|
48
|
+
],
|
|
49
|
+
}),
|
|
50
|
+
replace({
|
|
51
|
+
values: {
|
|
52
|
+
"process.env.NODE_ENV": "false", // added this when bundling gave an 'process is not defined' error
|
|
53
|
+
},
|
|
54
|
+
}),
|
|
55
|
+
resolve({
|
|
56
|
+
ignoreGlobal: false,
|
|
57
|
+
include: ["node_modules/**"],
|
|
58
|
+
dedupe: ["preact"],
|
|
59
|
+
browser: true
|
|
60
|
+
}),
|
|
61
|
+
commonjs({ transformMixedEsModules: true }), // preact was imported twice, with require (unistore/preact) and with import (our import), which resulted in 2 preacts in our project
|
|
62
|
+
json(), // used to import config.json
|
|
63
|
+
typescript({
|
|
64
|
+
tsconfig: `${PACKAGE_ROOT_PATH}/tsconfig.json`,
|
|
65
|
+
declarationDir: `${PACKAGE_ROOT_PATH}/dist`
|
|
66
|
+
}),
|
|
67
|
+
// litTailwind({
|
|
68
|
+
// include: "src/components/**/*.ts",
|
|
69
|
+
// }),
|
|
70
|
+
postcss({
|
|
71
|
+
extract: false, // not extracting embeds the js in the build, which is what we need for shipping it
|
|
72
|
+
modules: false, // tried modules at first, could work, but not with html in the qti file
|
|
73
|
+
inject: false, // does not inject the imported styles in the head
|
|
74
|
+
extensions: [".css"],
|
|
75
|
+
}),
|
|
76
|
+
copy({
|
|
77
|
+
targets: [
|
|
78
|
+
{ src: "src/assets", dest: "dist" }, // copy asses to dist folder
|
|
79
|
+
],
|
|
80
|
+
}),
|
|
81
|
+
image(), // import images png/svg/gif/jpg and converts to inline code
|
|
82
|
+
],
|
|
83
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { terser } from 'rollup-plugin-terser'
|
|
2
|
+
import rollupConfig from "./rollup.config";
|
|
3
|
+
|
|
4
|
+
const newRollupConfig = {
|
|
5
|
+
...rollupConfig,
|
|
6
|
+
output: rollupConfig.output.map((output) => ({ ...output, sourcemap: false })),
|
|
7
|
+
plugins: [...rollupConfig.plugins, terser()],
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export default newRollupConfig;
|
|
Binary file
|
package/types/index.d.ts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
export interface IMSpci<ConfigProperties> {
|
|
2
|
+
typeIdentifier: string;
|
|
3
|
+
|
|
4
|
+
/** @access public
|
|
5
|
+
* @method getInstance Create a new instance of this portable custom interaction
|
|
6
|
+
* Will be called by the qtiCustomInteractionContext
|
|
7
|
+
* @param {DOM Element} dom - the DOM Element this PCI is being added to
|
|
8
|
+
* @param {Object} configuration - the configuration to apply to this PCI
|
|
9
|
+
* @param {String} state - a previous saved state to apply to this instance.
|
|
10
|
+
* This must have been obtained from a prior call to getState on an
|
|
11
|
+
* instance of this type (same typeIdentifier)
|
|
12
|
+
*/
|
|
13
|
+
getInstance: (dom: HTMLElement, configuration: Configuration<ConfigProperties>, state: string) => void;
|
|
14
|
+
|
|
15
|
+
/** @access public
|
|
16
|
+
* @method getResponse
|
|
17
|
+
* @return {Object} - the value to assign to the bound QTI response variable
|
|
18
|
+
*/
|
|
19
|
+
getResponse: () => QtiVariableJSON;
|
|
20
|
+
|
|
21
|
+
/** @access public
|
|
22
|
+
* @method getState
|
|
23
|
+
* @return {String} The current state of this PCI. May be passed to
|
|
24
|
+
* getInstance to later restore this PCI instance.
|
|
25
|
+
*/
|
|
26
|
+
getState: () => string;
|
|
27
|
+
oncompleted?: () => void;
|
|
28
|
+
destroy?: () => void; // Not used in IMS and not in TAO implementation, so not used here (optional)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
export declare type Configuration<T> = {
|
|
34
|
+
onready: (pci: IMSpci<T>, state?: string) => void;
|
|
35
|
+
properties: T;
|
|
36
|
+
// PK: following this: https://www.imsglobal.org/spec/qti/v3p0/impl#h.1mc9puik2ft6
|
|
37
|
+
// All below are not used in the IMS Reference implementation, keeping for ref
|
|
38
|
+
/*
|
|
39
|
+
ondone: (pci: IMSPci<T>,response:any,state:string, status?: "interacting" | "closed" | "solution" | "review" ) => void;
|
|
40
|
+
status: string;
|
|
41
|
+
templateVariables: any;
|
|
42
|
+
boundTo: any;
|
|
43
|
+
contextVariables
|
|
44
|
+
responseIdentifier
|
|
45
|
+
*/
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export interface directedPair {
|
|
49
|
+
destination: string;
|
|
50
|
+
source: string;
|
|
51
|
+
}
|
|
52
|
+
export interface ResponseInteraction<T> {
|
|
53
|
+
responseIdentifier: string;
|
|
54
|
+
responses: T;
|
|
55
|
+
}
|
|
56
|
+
export interface ResponseItem<T> {
|
|
57
|
+
item: string;
|
|
58
|
+
interaction: ResponseInteraction<T>;
|
|
59
|
+
}
|
|
60
|
+
export interface InputResponseFormat<T> {
|
|
61
|
+
item: string;
|
|
62
|
+
interactions: ResponseInteraction<T>[];
|
|
63
|
+
}
|
|
64
|
+
export interface Calculate {
|
|
65
|
+
calculate: () => ResponseType;
|
|
66
|
+
}
|
|
67
|
+
export declare type QtiVariableJSON = {
|
|
68
|
+
[K in "list" | "base"]?: {
|
|
69
|
+
[Ka in "boolean" | "integer" | "float" | "string" | "pair" | "directedPair" | "identifier"]?: ResponseType;
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
export declare type float = number;
|
|
73
|
+
export declare type integer = number;
|
|
74
|
+
export declare type ResponseType = boolean | directedPair | float | integer | string | ResponseType[];
|
|
75
|
+
export declare enum BaseType {
|
|
76
|
+
boolean = "boolean",
|
|
77
|
+
directedPair = "directedPair",
|
|
78
|
+
float = "float",
|
|
79
|
+
integer = "integer",
|
|
80
|
+
string = "string",
|
|
81
|
+
}
|
|
82
|
+
export declare type Multiple = Array<ResponseType>;
|
|
83
|
+
export declare type Ordered = Array<ResponseType>;
|
|
84
|
+
export declare enum Cardinality {
|
|
85
|
+
multiple = "multiple",
|
|
86
|
+
ordered = "ordered",
|
|
87
|
+
single = "single",
|
|
88
|
+
}
|