@citolab/tspci 2.3.3 → 2.4.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/preset/preact/src/index.tsx +6 -4
- package/preset/preact/src/interaction.tsx +8 -11
- package/preset/preact/src/store.ts +12 -9
- package/public/index.html +271 -214
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@citolab/tspci",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0-alpha.0",
|
|
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",
|
|
@@ -46,5 +46,5 @@
|
|
|
46
46
|
"help": "tspci --help",
|
|
47
47
|
"init": "tspci init --force --path=../../pcis"
|
|
48
48
|
},
|
|
49
|
-
"gitHead": "
|
|
49
|
+
"gitHead": "881b3b40f3e31eafdc38ff79b51a16384e67f89d"
|
|
50
50
|
}
|
|
@@ -7,7 +7,7 @@ import Interaction from "./interaction";
|
|
|
7
7
|
import style from "./styles.css";
|
|
8
8
|
import { Configuration, IMSpci, QtiVariableJSON } from "@citolab/tspci";
|
|
9
9
|
import configProps from "./config.json";
|
|
10
|
-
import {
|
|
10
|
+
import { IStore } from "@citolab/preact-store";
|
|
11
11
|
|
|
12
12
|
type PropTypes = typeof configProps;
|
|
13
13
|
|
|
@@ -16,7 +16,7 @@ class App implements IMSpci<PropTypes> {
|
|
|
16
16
|
config: Configuration<PropTypes>; // reference to the interface of the config object which you get when getInstance is called by the player
|
|
17
17
|
state: string; // keep a reference to the state
|
|
18
18
|
shadowdom: ShadowRoot; // Not mandatory, but its wise to create a shadowroot
|
|
19
|
-
store:
|
|
19
|
+
store: IStore<StateModel>;
|
|
20
20
|
|
|
21
21
|
private logActions: { type: string; payload: unknown }[] = []; // optional logActions
|
|
22
22
|
|
|
@@ -30,8 +30,10 @@ class App implements IMSpci<PropTypes> {
|
|
|
30
30
|
|
|
31
31
|
const initState: StateModel = stateString ? JSON.parse(stateString).state : { input: 0 };
|
|
32
32
|
this.logActions = stateString ? JSON.parse(stateString).log : [];
|
|
33
|
-
this.store = initStore
|
|
34
|
-
|
|
33
|
+
this.store = initStore(this.initialState );
|
|
34
|
+
if (restoredState || logActions) {
|
|
35
|
+
this.store.restoreState(restoredState, logActions);
|
|
36
|
+
}
|
|
35
37
|
this.shadowdom = dom.attachShadow({ mode: "closed" });
|
|
36
38
|
this.render();
|
|
37
39
|
|
|
@@ -1,16 +1,13 @@
|
|
|
1
|
-
import { useStore } from "@citolab/preact-store";
|
|
1
|
+
import { useStore, IStore, Action } from "@citolab/preact-store";
|
|
2
2
|
import { ActionType, StateModel } from "./store";
|
|
3
3
|
import configProps from "./config.json";
|
|
4
4
|
import procenten from "./assets/procenten.png"; // image types are bundled inside the js
|
|
5
5
|
|
|
6
6
|
type PropTypes = typeof configProps;
|
|
7
7
|
|
|
8
|
-
const Interaction = ({ config, dom }: { config: PropTypes; dom: Document | ShadowRoot }) => {
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
console.log(`type: ${type}, payload: ${JSON.stringify(payload)}`);
|
|
12
|
-
}
|
|
13
|
-
);
|
|
8
|
+
const Interaction = ({ config, dom }: { config: PropTypes; dom: Document | ShadowRoot, store: IStore<StateModel> }) => {
|
|
9
|
+
const state = useStore(store);
|
|
10
|
+
|
|
14
11
|
return <div className="pci-container">
|
|
15
12
|
<h1>{config.title}</h1>
|
|
16
13
|
<div className="body">
|
|
@@ -21,12 +18,12 @@ const Interaction = ({ config, dom }: { config: PropTypes; dom: Document | Shado
|
|
|
21
18
|
<input type="number"
|
|
22
19
|
onKeyUp={(e) => {
|
|
23
20
|
const input = e.target as HTMLInputElement;
|
|
24
|
-
dispatch<{ input: number }>("SET_INPUT", { input: +input.value });
|
|
21
|
+
store.dispatch<{ input: number }>({ type: "SET_INPUT", payload: { input: +input.value } });
|
|
22
|
+
}
|
|
25
23
|
}
|
|
26
|
-
|
|
27
|
-
onChange={(e) => {
|
|
24
|
+
onChange={(e) => {
|
|
28
25
|
const input = e.target as HTMLInputElement;
|
|
29
|
-
dispatch<{ input: number }>("SET_INPUT", { input: +input.value });
|
|
26
|
+
store.dispatch<{ input: number }>({ type: "SET_INPUT", payload: { input: +input.value } });
|
|
30
27
|
}} min="0" max="100" />%
|
|
31
28
|
</div>
|
|
32
29
|
</div>;
|
|
@@ -1,12 +1,15 @@
|
|
|
1
|
-
|
|
1
|
+
import { Store } from "@citolab/preact-store";
|
|
2
2
|
|
|
3
|
+
export type ActionType = "SET_INPUT" | "RESTORE_STATE";
|
|
3
4
|
export type StateModel = { input: number };
|
|
4
5
|
|
|
5
|
-
export const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
6
|
+
export const initStore = (initialState: StateModel, restoreData?: {
|
|
7
|
+
state: StateModel;
|
|
8
|
+
actions: { type: string; payload: any; timestamp?: number }[];
|
|
9
|
+
}) => {
|
|
10
|
+
const store = new Store<StateModel>(initialState, restoreData);
|
|
11
|
+
store.addReducer<{ input: number }>("SET_INPUT", (state, { input }) => {
|
|
12
|
+
return {...state, input };
|
|
13
|
+
});
|
|
14
|
+
return store;
|
|
15
|
+
}
|
package/public/index.html
CHANGED
|
@@ -1,152 +1,203 @@
|
|
|
1
1
|
<!DOCTYPE html>
|
|
2
2
|
<html lang="en">
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
id="noresponse"
|
|
47
|
-
>
|
|
48
|
-
user has not yet interacted yet, getResponse should return undefined.
|
|
49
|
-
</div>
|
|
50
|
-
|
|
51
|
-
<div id="pciContainer" class="shadow-sm">
|
|
52
|
-
<div id="loading" class="text-muted mx-auto p-4">Loading...</div>
|
|
53
|
-
</div>
|
|
54
|
-
<div id="devtools"></div>
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<title>Hello RequireJS</title>
|
|
6
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.6/require.min.js"
|
|
7
|
+
integrity="sha512-c3Nl8+7g4LMSTdrm621y7kf9v3SDPnhxLNhcjFJbKECVnmZHTdo+IRO05sNLTH/D3vA6u1X32ehoLC7WFVdheg=="
|
|
8
|
+
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
|
9
|
+
<!-- <link rel="stylesheet" class="bootswatcher" href="//bootswatch.com/5/lumen/bootstrap.min.css" /> -->
|
|
10
|
+
|
|
11
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" />
|
|
12
|
+
|
|
13
|
+
<style>
|
|
14
|
+
html {
|
|
15
|
+
font-size: 100%;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
#pciContainer {
|
|
19
|
+
width: 100%;
|
|
20
|
+
/* min-width: 800px; */
|
|
21
|
+
|
|
22
|
+
max-width: 1024px;
|
|
23
|
+
height: 600px;
|
|
24
|
+
min-height: 600px;
|
|
25
|
+
/* max-height: 600px; */
|
|
26
|
+
border: 1px solid #eeeeee;
|
|
27
|
+
overflow: hidden;
|
|
28
|
+
margin: 0 auto;
|
|
29
|
+
background-color: white;
|
|
30
|
+
border-radius: 10px;
|
|
31
|
+
margin-top: 1rem;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
#devtools {
|
|
35
|
+
max-width: 1024px;
|
|
36
|
+
margin: 0 auto;
|
|
37
|
+
}
|
|
38
|
+
</style>
|
|
39
|
+
</head>
|
|
40
|
+
|
|
41
|
+
<body class="bg-light">
|
|
42
|
+
<div class="container">
|
|
43
|
+
<div class="alert alert-danger position-fixed mx-auto left-2 top-2" style="display: none" role="alert"
|
|
44
|
+
id="noresponse">
|
|
45
|
+
user has not yet interacted yet<br />getResponse should return undefined.
|
|
55
46
|
</div>
|
|
56
47
|
|
|
57
|
-
<
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const html = htm.bind(h);
|
|
63
|
-
|
|
64
|
-
function App({ item }) {
|
|
65
|
-
const [show, setShow] = useState(false);
|
|
66
|
-
|
|
67
|
-
let keyArray = [];
|
|
68
|
-
if (item.config && item.config.properties) {
|
|
69
|
-
keyArray = Object.keys(item.config.properties).map((keyName, keyIndex) => ({
|
|
70
|
-
key: keyName,
|
|
71
|
-
value: item.config.properties[keyName],
|
|
72
|
-
}));
|
|
73
|
-
}
|
|
48
|
+
<div id="pciContainer" class="shadow-sm">
|
|
49
|
+
<div id="loading" class="text-muted mx-auto p-4">Loading...</div>
|
|
50
|
+
</div>
|
|
51
|
+
<div id="devtools"></div>
|
|
52
|
+
</div>
|
|
74
53
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
54
|
+
<script type="module">
|
|
55
|
+
import { h, Component, render } from "https://unpkg.com/preact@latest?module";
|
|
56
|
+
import { useState, useEffect, useRef } from "https://unpkg.com/preact@latest/hooks/dist/hooks.module.js?module";
|
|
57
|
+
import htm from "https://unpkg.com/htm?module";
|
|
58
|
+
// Initialize htm with Preact
|
|
59
|
+
const html = htm.bind(h);
|
|
80
60
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
61
|
+
/**
|
|
62
|
+
* Minified useInterval hook. A custom hook that calls the given `callback` function at the given `delay` interval.
|
|
63
|
+
* useInterval(() => {
|
|
64
|
+
* console.log('interval')
|
|
65
|
+
* }, 1000);
|
|
66
|
+
* The callback is not called immediately on mount but only after the first delay.
|
|
67
|
+
*/
|
|
68
|
+
function useInterval(c, d) { const s = useRef(); useEffect(() => { s.current = c }, [c]), useEffect(() => { if (d !== null) { const i = setInterval(() => { s.current() }, d); return () => clearInterval(i) } }, [d]) }
|
|
85
69
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
item.resetResponse();
|
|
89
|
-
}
|
|
70
|
+
function App({ item }) {
|
|
71
|
+
const [show, setShow] = useState(false);
|
|
90
72
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
} else {
|
|
100
|
-
alert("no response stored");
|
|
101
|
-
}
|
|
102
|
-
}
|
|
73
|
+
const [range, setRange] = useState(0);
|
|
74
|
+
const [currentRangeValue, setCurrentRangeValue] = useState(0);
|
|
75
|
+
const [maxRange, setMaxRange] = useState(0);
|
|
76
|
+
const [actions, setActions] = useState([]);
|
|
77
|
+
|
|
78
|
+
const isReplaying = useRef(false);
|
|
79
|
+
const replayedIndex = useRef(0);
|
|
80
|
+
const store = item.store;
|
|
103
81
|
|
|
104
|
-
|
|
105
|
-
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
if (store) {
|
|
84
|
+
store.subscribeActions(action => {
|
|
85
|
+
if (!isReplaying.current) {
|
|
86
|
+
const allActions = store.actions;
|
|
87
|
+
setRange(allActions.length);
|
|
88
|
+
setMaxRange(allActions.length);
|
|
89
|
+
setActions(allActions);
|
|
90
|
+
setCurrentRangeValue(allActions.findIndex(a => a === action) + 1);
|
|
91
|
+
} else {
|
|
92
|
+
replayedIndex.current++;
|
|
93
|
+
setCurrentRangeValue(replayedIndex.current);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
106
96
|
}
|
|
97
|
+
}, [store]);
|
|
107
98
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
99
|
+
async function scrubRange(value) {
|
|
100
|
+
if (!isReplaying.current) {
|
|
101
|
+
setCurrentRangeValue(value);
|
|
102
|
+
resetResponse();
|
|
103
|
+
if (store) {
|
|
104
|
+
isReplaying.current = true;
|
|
105
|
+
replayedIndex.current = 0;
|
|
106
|
+
setCurrentRangeValue(value);
|
|
107
|
+
await store.replay([...actions], { until: value, animate: false });
|
|
108
|
+
isReplaying.current = false;
|
|
118
109
|
}
|
|
119
110
|
}
|
|
111
|
+
}
|
|
120
112
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
113
|
+
let keyArray = [];
|
|
114
|
+
if (item.config && item.config.properties) {
|
|
115
|
+
keyArray = Object.keys(item.config.properties).map((keyName, keyIndex) => ({
|
|
116
|
+
key: keyName,
|
|
117
|
+
value: item.config.properties[keyName],
|
|
118
|
+
}));
|
|
119
|
+
}
|
|
126
120
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
121
|
+
function resetlocalstorage() {
|
|
122
|
+
localStorage.removeItem(item.typeIdentifier + "_state");
|
|
123
|
+
localStorage.removeItem(item.typeIdentifier + "_props");
|
|
124
|
+
localStorage.removeItem(item.typeIdentifier + "_response");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function reset() {
|
|
128
|
+
resetlocalstorage();
|
|
129
|
+
location.reload();
|
|
130
|
+
}
|
|
131
131
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
132
|
+
function resetResponse() {
|
|
133
|
+
localStorage.removeItem(item.typeIdentifier + "_response");
|
|
134
|
+
item.resetResponse();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function setResponse() {
|
|
138
|
+
const response = localStorage.getItem(item.typeIdentifier + "_response");
|
|
139
|
+
if (response) {
|
|
140
|
+
item.setResponse({
|
|
141
|
+
base: {
|
|
142
|
+
string: response,
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
} else {
|
|
146
|
+
alert("no response stored");
|
|
135
147
|
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function saveResponse() {
|
|
151
|
+
localStorage.setItem(item.typeIdentifier + "_response", item.getResponse().base.string);
|
|
152
|
+
}
|
|
136
153
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
154
|
+
function compareResponse() {
|
|
155
|
+
const response = localStorage.getItem(item.typeIdentifier + "_response");
|
|
156
|
+
if (!response) {
|
|
157
|
+
alert("no response stored");
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if (response === item.getResponse().base.string) {
|
|
161
|
+
alert("same");
|
|
162
|
+
} else {
|
|
163
|
+
alert("different");
|
|
140
164
|
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function reload() {
|
|
168
|
+
const state = item.getState();
|
|
169
|
+
localStorage.setItem(item.typeIdentifier + "_state", JSON.stringify(state));
|
|
170
|
+
location.reload();
|
|
171
|
+
}
|
|
141
172
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
173
|
+
async function playState() {
|
|
174
|
+
if (store && !isReplaying.current) {
|
|
175
|
+
resetResponse();
|
|
176
|
+
isReplaying.current = true;
|
|
177
|
+
replayedIndex.current = 0;
|
|
178
|
+
await store.replay([...actions]);
|
|
179
|
+
isReplaying.current = false;
|
|
147
180
|
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function changeConfig(key, value) {
|
|
184
|
+
item.config.properties[key] = value;
|
|
185
|
+
item.trigger(`change${key}`, value);
|
|
186
|
+
}
|
|
148
187
|
|
|
149
|
-
|
|
188
|
+
function reConfig(key, value) {
|
|
189
|
+
localStorage.setItem(item.typeIdentifier + "_props", JSON.stringify(item.config.properties));
|
|
190
|
+
location.reload();
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function unload() {
|
|
194
|
+
item.destroy && item.destroy();
|
|
195
|
+
item.oncompleted && item.oncompleted();
|
|
196
|
+
var name = require.undef("index.js");
|
|
197
|
+
loadItem();
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return html`
|
|
150
201
|
<div>
|
|
151
202
|
<div style="width:300px" class="shadow offcanvas offcanvas-end ${show ? "show visible" : ""}">
|
|
152
203
|
<div class="offcanvas-header">
|
|
@@ -161,7 +212,7 @@
|
|
|
161
212
|
<div class="offcanvas-body">
|
|
162
213
|
<ul class="card-body">
|
|
163
214
|
${keyArray.map(
|
|
164
|
-
|
|
215
|
+
({ key, value }) => html`<div>
|
|
165
216
|
<div>${key}</div>
|
|
166
217
|
<input
|
|
167
218
|
type="text"
|
|
@@ -170,27 +221,21 @@
|
|
|
170
221
|
onkeyup="${(e) => changeConfig(key, e.target.value)}"
|
|
171
222
|
/>
|
|
172
223
|
</div>`
|
|
173
|
-
|
|
224
|
+
)}
|
|
174
225
|
</ul>
|
|
175
226
|
<button class="btn btn-primary" onClick="${() => reConfig()}">save and reload</button>
|
|
176
227
|
</div>
|
|
177
228
|
</div>
|
|
178
229
|
|
|
179
|
-
<div class="d-flex justify-content-
|
|
180
|
-
<div>
|
|
230
|
+
<div class="d-flex justify-content-between mt-3" role="toolbar" aria-label="Toolbar with button groups">
|
|
231
|
+
<div class="d-flex flex-column">
|
|
181
232
|
<small class="text-muted">configuration</small>
|
|
182
233
|
<div class="btn-group me-2 shadow-sm" role="group" aria-label="Basic checkbox toggle button group">
|
|
183
|
-
<button class="btn btn-outline-secondary" onClick="${() => setShow(true)}">
|
|
184
|
-
<button
|
|
185
|
-
class="btn btn-outline-secondary"
|
|
186
|
-
onClick="${() => alert(JSON.stringify(item.config), null, 4)}"
|
|
187
|
-
>
|
|
188
|
-
config
|
|
189
|
-
</button>
|
|
234
|
+
<button class="btn btn-outline-secondary" onClick="${() => setShow(true)}">config</button>
|
|
190
235
|
</div>
|
|
191
236
|
</div>
|
|
192
237
|
|
|
193
|
-
<div>
|
|
238
|
+
<div class="d-flex flex-column">
|
|
194
239
|
<small class="text-muted">data</small>
|
|
195
240
|
<div class="btn-group me-2 shadow-sm" role="group" aria-label="Basic checkbox toggle button group">
|
|
196
241
|
<button
|
|
@@ -203,29 +248,30 @@
|
|
|
203
248
|
</div>
|
|
204
249
|
</div>
|
|
205
250
|
|
|
206
|
-
<div>
|
|
251
|
+
<div class="d-flex flex-column">
|
|
207
252
|
<small class="text-muted">state</small>
|
|
208
253
|
<div class="btn-group me-2 shadow-sm" role="group" aria-label="Basic checkbox toggle button group">
|
|
209
254
|
<button class="btn btn-outline-secondary" onClick="${() => reload()}">reload</button>
|
|
210
255
|
</div>
|
|
211
256
|
</div>
|
|
212
|
-
|
|
213
|
-
|
|
257
|
+
|
|
258
|
+
<div class="d-flex flex-column">
|
|
259
|
+
<small class="text-muted">scoring TAO (match correct string)</small>
|
|
214
260
|
<div class="btn-group me-2 shadow-sm" role="group" aria-label="Basic checkbox toggle button group">
|
|
215
|
-
<button class="btn btn-outline-secondary" onClick="${() => saveResponse()}">
|
|
261
|
+
<button class="btn btn-outline-secondary" onClick="${() => saveResponse()}">save</button>
|
|
216
262
|
<button class="btn btn-outline-secondary" onClick="${() => compareResponse()}">compare</button>
|
|
217
|
-
<button class="btn btn-outline-secondary" onClick="${() => setResponse()}">
|
|
263
|
+
<button class="btn btn-outline-secondary" onClick="${() => setResponse()}">load</button>
|
|
218
264
|
<button
|
|
219
265
|
class="btn btn-outline-secondary"
|
|
220
266
|
disabled="${!item.resetResponse}"
|
|
221
267
|
onClick="${() => resetResponse()}"
|
|
222
268
|
>
|
|
223
|
-
|
|
269
|
+
clear
|
|
224
270
|
</button>
|
|
225
271
|
</div>
|
|
226
272
|
</div>
|
|
227
273
|
|
|
228
|
-
<div>
|
|
274
|
+
<div class="d-flex flex-column">
|
|
229
275
|
<small class="text-muted">ㅤ</small>
|
|
230
276
|
<div class="d-flex">
|
|
231
277
|
<button
|
|
@@ -239,80 +285,91 @@
|
|
|
239
285
|
</div>
|
|
240
286
|
</div>
|
|
241
287
|
</div>
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
288
|
+
${store && html`<div class="mt-5 d-flex flex-column gap-2">
|
|
289
|
+
<div class="d-flex justify-content-between">
|
|
290
|
+
<button class="btn btn-outline-secondary" onClick="${() => playState()}">
|
|
291
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-play" viewBox="0 0 16 16">
|
|
292
|
+
<path d="M3.905 14.21a1 1 0 0 1-1.555-.832V2.622a1 1 0 0 1 1.555-.832l9.066 5.293a1 1 0 0 1 0 1.664l-9.066 5.293zm-.57-1.664L11.4 8 3.335 2.454v10.092z"/>
|
|
293
|
+
</svg>
|
|
294
|
+
</button>
|
|
295
|
+
<span>${currentRangeValue}</span>
|
|
296
|
+
</div>
|
|
297
|
+
<div class="d-flex gap-2">
|
|
298
|
+
<span>0</span>
|
|
299
|
+
<input
|
|
300
|
+
class="form-control-range flex-grow-1"
|
|
301
|
+
type="range"
|
|
302
|
+
min="0"
|
|
303
|
+
max="${maxRange}"
|
|
304
|
+
value=${currentRangeValue}
|
|
305
|
+
onInput=${(e) => scrubRange(e.target.value)}
|
|
306
|
+
step="1"
|
|
307
|
+
/>
|
|
308
|
+
<span>${maxRange}</span>
|
|
309
|
+
</div>
|
|
310
|
+
</div>` }
|
|
255
311
|
|
|
256
312
|
</div>
|
|
257
313
|
`;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function register(item) {
|
|
317
|
+
let state = undefined;
|
|
318
|
+
const localStorageState = localStorage.getItem(item.typeIdentifier + "_state");
|
|
319
|
+
if (localStorageState) {
|
|
320
|
+
state = JSON.parse(localStorageState);
|
|
258
321
|
}
|
|
322
|
+
let props = {};
|
|
323
|
+
const localStorageProps = localStorage.getItem(item.typeIdentifier + "_props");
|
|
324
|
+
if (localStorageProps) {
|
|
325
|
+
props = JSON.parse(localStorageProps);
|
|
326
|
+
}
|
|
327
|
+
const dom = document.getElementById("pciContainer");
|
|
328
|
+
const config = {
|
|
329
|
+
onready: () => {
|
|
330
|
+
document.getElementById("loading").innerHTML = "";
|
|
331
|
+
},
|
|
332
|
+
properties: props,
|
|
333
|
+
};
|
|
259
334
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
state = JSON.parse(localStorageState);
|
|
265
|
-
}
|
|
266
|
-
let props = {};
|
|
267
|
-
const localStorageProps = localStorage.getItem(item.typeIdentifier + "_props");
|
|
268
|
-
if (localStorageProps) {
|
|
269
|
-
props = JSON.parse(localStorageProps);
|
|
270
|
-
}
|
|
271
|
-
const dom = document.getElementById("pciContainer");
|
|
272
|
-
const config = {
|
|
273
|
-
onready: () => {
|
|
274
|
-
document.getElementById("loading").innerHTML = "";
|
|
275
|
-
},
|
|
276
|
-
properties: props,
|
|
277
|
-
};
|
|
278
|
-
|
|
279
|
-
item.getInstance(dom, config, state);
|
|
280
|
-
|
|
281
|
-
if (item.getResponse() !== undefined) {
|
|
282
|
-
document.getElementById("noresponse").style.display = "block";
|
|
283
|
-
}
|
|
284
|
-
localStorage.removeItem(item.typeIdentifier + "_state");
|
|
335
|
+
item.getInstance(dom, config, state);
|
|
336
|
+
|
|
337
|
+
if (item.getResponse() !== undefined) {
|
|
338
|
+
document.getElementById("noresponse").style.display = "block";
|
|
285
339
|
}
|
|
340
|
+
localStorage.removeItem(item.typeIdentifier + "_state");
|
|
341
|
+
}
|
|
286
342
|
|
|
287
|
-
|
|
288
|
-
|
|
343
|
+
define("qtiCustomInteractionContext", (qtiCustomInteractionContext) => {
|
|
344
|
+
return { register: (ctxA) => register(ctxA) };
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
function loadItem() {
|
|
348
|
+
requirejs(["index.js"], (itemInstance) => {
|
|
349
|
+
render(html`<${App} item="${itemInstance}" />`, document.getElementById("devtools"));
|
|
289
350
|
});
|
|
351
|
+
}
|
|
352
|
+
window.onload = () => loadItem();
|
|
353
|
+
</script>
|
|
290
354
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
</
|
|
298
|
-
|
|
299
|
-
<
|
|
300
|
-
<
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
<li>Implements on off() methods of tao?</li>
|
|
313
|
-
<li>Response type and response correct</li>
|
|
314
|
-
<li>getResponse() should give undefined when user has not done anything</li>
|
|
315
|
-
<li>For TAO to enter response is the entered comparable by the response given</li>
|
|
316
|
-
</ul>
|
|
317
|
-
</body>
|
|
318
|
-
</html>
|
|
355
|
+
<h3 style="margin-top: 12rem">checklist</h3>
|
|
356
|
+
<ul style="margin-bottom: 10rem">
|
|
357
|
+
<li>Is the css scoped to the item?</li>
|
|
358
|
+
<li>Is the css/item somewhat responsive on smaller screens</li>
|
|
359
|
+
<li>Can you reset the item to its initial state</li>
|
|
360
|
+
<li>Can it restore its state ( reload )</li>
|
|
361
|
+
<li>Can you change the config</li>
|
|
362
|
+
<li>Colorblind</li>
|
|
363
|
+
<li>Touch/Mouse friendly</li>
|
|
364
|
+
<li>Works in older browser like IE11</li>
|
|
365
|
+
<li>Destroys itself correctly, removing event listeners</li>
|
|
366
|
+
<li>No objects or variables on global scope</li>
|
|
367
|
+
<li>Triggers correctly on changed config properties</li>
|
|
368
|
+
<li>Implements on off() methods of tao?</li>
|
|
369
|
+
<li>Response type and response correct</li>
|
|
370
|
+
<li>getResponse() should give undefined when user has not done anything</li>
|
|
371
|
+
<li>For TAO to enter response is the entered comparable by the response given</li>
|
|
372
|
+
</ul>
|
|
373
|
+
</body>
|
|
374
|
+
|
|
375
|
+
</html>
|