virtual-keypad 5.13.0 → 5.13.2
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/LICENSE +21 -21
- package/README.md +45 -45
- package/dist/404.html +14 -14
- package/dist/fonts/Pelli-EyeChart.svg +494 -494
- package/dist/fonts/Sloan.svg +138 -138
- package/dist/index.html +22 -22
- package/dist/keypad.html +58 -58
- package/dist/main.js +1 -1
- package/dist/main.js.LICENSE.txt +8 -8
- package/dist/receiver.html +146 -146
- package/dist/server.js +66 -66
- package/package.json +36 -36
- package/src/keypad.css +108 -108
- package/src/keypad.js +399 -385
- package/src/keypadPeer.js +191 -189
- package/src/main.js +4 -4
- package/src/maxKeySize.js +142 -105
- package/src/receiver.css +11 -11
- package/src/receiver.js +257 -223
- package/webpack.common.js +30 -30
- package/webpack.dev.js +9 -9
- package/webpack.prod.js +6 -6
package/src/keypad.js
CHANGED
|
@@ -1,385 +1,399 @@
|
|
|
1
|
-
import { applyMaxKeySize } from "./maxKeySize";
|
|
2
|
-
import "./keypad.css";
|
|
3
|
-
import { KeypadPeer, keypadUrl } from "./keypadPeer.js";
|
|
4
|
-
|
|
5
|
-
class Keypad extends KeypadPeer {
|
|
6
|
-
constructor(
|
|
7
|
-
keypadParameters = {
|
|
8
|
-
keypadUrl: keypadUrl,
|
|
9
|
-
targetElementId: null,
|
|
10
|
-
visualResponseFeedback: false,
|
|
11
|
-
}
|
|
12
|
-
) {
|
|
13
|
-
super({
|
|
14
|
-
keypadUrl: keypadParameters.keypadUrl,
|
|
15
|
-
targetElementId: keypadParameters.targetElementId,
|
|
16
|
-
});
|
|
17
|
-
this.startTime = Date.now();
|
|
18
|
-
this.receiverPeerId = null;
|
|
19
|
-
|
|
20
|
-
const parametersFromURL = this.parseParams(
|
|
21
|
-
new URLSearchParams(window.location.search)
|
|
22
|
-
);
|
|
23
|
-
// this.alphabet = this.checkAlphabet(parametersFromURL.alphabet);
|
|
24
|
-
// this.font = parametersFromURL.font;
|
|
25
|
-
this.receiverPeerId = parametersFromURL.peerId;
|
|
26
|
-
// this.visualResponseFeedback = keypadParameters.visualResponseFeedback;
|
|
27
|
-
|
|
28
|
-
// Set-up sound to play on press
|
|
29
|
-
this.pressFeedback = new Audio(this.keypressFeedbackSound);
|
|
30
|
-
|
|
31
|
-
this.peer.on("open", this.#onPeerOpen);
|
|
32
|
-
this.peer.on("connection", this.#disallowIncomingConnections);
|
|
33
|
-
this.peer.on("disconnected", this.onPeerDisconnected);
|
|
34
|
-
this.peer.on("close", this.onPeerClose);
|
|
35
|
-
this.peer.on("error", this.onPeerError);
|
|
36
|
-
|
|
37
|
-
this.#prepareHTML();
|
|
38
|
-
}
|
|
39
|
-
#onPeerOpen = (id) => {
|
|
40
|
-
// Workaround for peer.reconnect deleting previous id
|
|
41
|
-
if (this.peer.id === null) {
|
|
42
|
-
console.log("Received null id from peer open");
|
|
43
|
-
this.peer.id = this.lastPeerId;
|
|
44
|
-
} else {
|
|
45
|
-
this.lastPeerId = this.peer.id;
|
|
46
|
-
}
|
|
47
|
-
this.#join();
|
|
48
|
-
};
|
|
49
|
-
#disallowIncomingConnections = (connection) => {
|
|
50
|
-
connection.on("open", function () {
|
|
51
|
-
connection.send({
|
|
52
|
-
message: "Rejected",
|
|
53
|
-
info: "Sender does not accept incoming connections"
|
|
54
|
-
});
|
|
55
|
-
setTimeout(function () {
|
|
56
|
-
connection.close();
|
|
57
|
-
}, 500);
|
|
58
|
-
});
|
|
59
|
-
};
|
|
60
|
-
#onConnData = (data) => {
|
|
61
|
-
console.log("Data received: ", data);
|
|
62
|
-
data = data; // data = JSON.parse(data);
|
|
63
|
-
switch (data.message) {
|
|
64
|
-
case "KeypadParameters":
|
|
65
|
-
this.alphabet = data.alphabet;
|
|
66
|
-
this.font = data.font;
|
|
67
|
-
this.#populateKeypad();
|
|
68
|
-
break;
|
|
69
|
-
case "UpdateHeader":
|
|
70
|
-
document.getElementById("keypad-header").innerText = data.headerContent;
|
|
71
|
-
document.getElementById("keypad-header").style.display =
|
|
72
|
-
|
|
73
|
-
this
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
this
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
* connection
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
this.conn.
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
*
|
|
147
|
-
* |
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
* |
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
keypadElem.appendChild(
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
console.log("
|
|
176
|
-
document.
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
window.
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
button
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
button.
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
header.
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
//
|
|
302
|
-
|
|
303
|
-
//
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
*
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
*
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
keyElems
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
1
|
+
import { applyMaxKeySize } from "./maxKeySize";
|
|
2
|
+
import "./keypad.css";
|
|
3
|
+
import { KeypadPeer, keypadUrl } from "./keypadPeer.js";
|
|
4
|
+
|
|
5
|
+
class Keypad extends KeypadPeer {
|
|
6
|
+
constructor(
|
|
7
|
+
keypadParameters = {
|
|
8
|
+
keypadUrl: keypadUrl,
|
|
9
|
+
targetElementId: null,
|
|
10
|
+
visualResponseFeedback: false,
|
|
11
|
+
}
|
|
12
|
+
) {
|
|
13
|
+
super({
|
|
14
|
+
keypadUrl: keypadParameters.keypadUrl,
|
|
15
|
+
targetElementId: keypadParameters.targetElementId,
|
|
16
|
+
});
|
|
17
|
+
this.startTime = Date.now();
|
|
18
|
+
this.receiverPeerId = null;
|
|
19
|
+
|
|
20
|
+
const parametersFromURL = this.parseParams(
|
|
21
|
+
new URLSearchParams(window.location.search)
|
|
22
|
+
);
|
|
23
|
+
// this.alphabet = this.checkAlphabet(parametersFromURL.alphabet);
|
|
24
|
+
// this.font = parametersFromURL.font;
|
|
25
|
+
this.receiverPeerId = parametersFromURL.peerId;
|
|
26
|
+
// this.visualResponseFeedback = keypadParameters.visualResponseFeedback;
|
|
27
|
+
|
|
28
|
+
// Set-up sound to play on press
|
|
29
|
+
this.pressFeedback = new Audio(this.keypressFeedbackSound);
|
|
30
|
+
|
|
31
|
+
this.peer.on("open", this.#onPeerOpen);
|
|
32
|
+
this.peer.on("connection", this.#disallowIncomingConnections);
|
|
33
|
+
this.peer.on("disconnected", this.onPeerDisconnected);
|
|
34
|
+
this.peer.on("close", this.onPeerClose);
|
|
35
|
+
this.peer.on("error", this.onPeerError);
|
|
36
|
+
|
|
37
|
+
this.#prepareHTML();
|
|
38
|
+
}
|
|
39
|
+
#onPeerOpen = (id) => {
|
|
40
|
+
// Workaround for peer.reconnect deleting previous id
|
|
41
|
+
if (this.peer.id === null) {
|
|
42
|
+
console.log("Received null id from peer open");
|
|
43
|
+
this.peer.id = this.lastPeerId;
|
|
44
|
+
} else {
|
|
45
|
+
this.lastPeerId = this.peer.id;
|
|
46
|
+
}
|
|
47
|
+
this.#join();
|
|
48
|
+
};
|
|
49
|
+
#disallowIncomingConnections = (connection) => {
|
|
50
|
+
connection.on("open", function () {
|
|
51
|
+
connection.send({
|
|
52
|
+
message: "Rejected",
|
|
53
|
+
info: "Sender does not accept incoming connections",
|
|
54
|
+
});
|
|
55
|
+
setTimeout(function () {
|
|
56
|
+
connection.close();
|
|
57
|
+
}, 500);
|
|
58
|
+
});
|
|
59
|
+
};
|
|
60
|
+
#onConnData = (data) => {
|
|
61
|
+
console.log("Data received: ", data);
|
|
62
|
+
data = data; // data = JSON.parse(data);
|
|
63
|
+
switch (data.message) {
|
|
64
|
+
case "KeypadParameters":
|
|
65
|
+
this.alphabet = data.alphabet;
|
|
66
|
+
this.font = data.font;
|
|
67
|
+
this.#populateKeypad();
|
|
68
|
+
break;
|
|
69
|
+
case "UpdateHeader":
|
|
70
|
+
document.getElementById("keypad-header").innerText = data.headerContent;
|
|
71
|
+
document.getElementById("keypad-header").style.display =
|
|
72
|
+
data.headerContent === "" ? "none" : "block";
|
|
73
|
+
this.headerMessage = data.headerContent;
|
|
74
|
+
this.#populateKeypad();
|
|
75
|
+
break;
|
|
76
|
+
case "UpdateFooter":
|
|
77
|
+
document.getElementById("keypad-footer").innerText = data.headerContent;
|
|
78
|
+
this.footerMessage = data.headerContent;
|
|
79
|
+
this.#populateKeypad();
|
|
80
|
+
break;
|
|
81
|
+
case "Update":
|
|
82
|
+
// Keypad has received data to update the keypad
|
|
83
|
+
if (!data.hasOwnProperty("alphabet") && !data.hasOwnProperty("font")) {
|
|
84
|
+
console.error(
|
|
85
|
+
'Error in parsing data received! Must set "alphabet" or "font" properties'
|
|
86
|
+
);
|
|
87
|
+
} else {
|
|
88
|
+
this.alphabet = data.alphabet ?? this.alphabet;
|
|
89
|
+
this.font = data.font ?? this.font;
|
|
90
|
+
}
|
|
91
|
+
this.#populateKeypad();
|
|
92
|
+
break;
|
|
93
|
+
case "Disable":
|
|
94
|
+
if (data.hasOwnProperty("keys")) {
|
|
95
|
+
// TODO check that data.keys is a list of strings, and "space" isn't ["s", "p", "a"...]
|
|
96
|
+
this.disableKeys(data.keys);
|
|
97
|
+
} else {
|
|
98
|
+
this.disableKeys();
|
|
99
|
+
}
|
|
100
|
+
break;
|
|
101
|
+
case "Enable":
|
|
102
|
+
if (data.hasOwnProperty("keys")) {
|
|
103
|
+
// TODO check that data.keys is a list of strings, and "space" isn't ["s", "p", "a"...]
|
|
104
|
+
this.enableKeys(data.keys);
|
|
105
|
+
} else {
|
|
106
|
+
this.enableKeys();
|
|
107
|
+
}
|
|
108
|
+
break;
|
|
109
|
+
// TODO factor out into keypadPeer
|
|
110
|
+
case "Heartbeat":
|
|
111
|
+
this.lastHeartbeat = performance.now();
|
|
112
|
+
break;
|
|
113
|
+
default:
|
|
114
|
+
console.log("Message type: ", data.message);
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
#join = () => {
|
|
118
|
+
/**
|
|
119
|
+
* Create the connection between the two Peers.
|
|
120
|
+
*
|
|
121
|
+
* Sets up callbacks that handle any events related to the
|
|
122
|
+
* connection and data received on it.
|
|
123
|
+
*/
|
|
124
|
+
// Close old connection
|
|
125
|
+
if (this.conn) {
|
|
126
|
+
this.conn.close();
|
|
127
|
+
}
|
|
128
|
+
// Create connection to destination peer specified by the query param
|
|
129
|
+
this.conn = this.peer.connect(this.receiverPeerId, {
|
|
130
|
+
reliable: true,
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
console.log("Connection: ", this.conn);
|
|
134
|
+
this.conn.on("open", this.#initiateHandshake);
|
|
135
|
+
// Handle incoming data (messages only since this is the signal sender)
|
|
136
|
+
this.conn.on("data", this.#onConnData);
|
|
137
|
+
// TODO figure out how to re-establish connection, or have more robust connection
|
|
138
|
+
this.conn.on("close", () => console.log("Connection closed"));
|
|
139
|
+
};
|
|
140
|
+
#initiateHandshake = () => {
|
|
141
|
+
this.conn.send({ message: "Handshake" });
|
|
142
|
+
this._setupHeartBeatIntervals();
|
|
143
|
+
};
|
|
144
|
+
#prepareHTML = () => {
|
|
145
|
+
/**
|
|
146
|
+
* ----------
|
|
147
|
+
* | Header |
|
|
148
|
+
* ----------
|
|
149
|
+
* |a b c d | <- keypad-keys
|
|
150
|
+
* |e f g h |
|
|
151
|
+
* |i j k l |
|
|
152
|
+
* ----------
|
|
153
|
+
* |space ret| <- keypad-control-keys
|
|
154
|
+
*/
|
|
155
|
+
// Keypad elem is a container for a message and all keys
|
|
156
|
+
const keypadElem = document.createElement("div");
|
|
157
|
+
keypadElem.setAttribute("id", "keypad");
|
|
158
|
+
const keypadHeader = document.createElement("h1");
|
|
159
|
+
keypadHeader.setAttribute("id", "keypad-header");
|
|
160
|
+
keypadHeader.classList.add("noselect");
|
|
161
|
+
const keypadKeys = document.createElement("div");
|
|
162
|
+
keypadKeys.setAttribute("id", "keypad-keys");
|
|
163
|
+
keypadKeys.classList.add("keys");
|
|
164
|
+
const keypadControlKeys = document.createElement("div");
|
|
165
|
+
keypadControlKeys.setAttribute("id", "keypad-control-keys");
|
|
166
|
+
keypadControlKeys.classList.add("keys");
|
|
167
|
+
const keypadFooter = document.createElement("div");
|
|
168
|
+
keypadFooter.setAttribute("id", "keypad-footer");
|
|
169
|
+
keypadElem.appendChild(keypadHeader);
|
|
170
|
+
keypadKeys.appendChild(keypadControlKeys);
|
|
171
|
+
keypadElem.appendChild(keypadKeys);
|
|
172
|
+
keypadElem.appendChild(keypadFooter);
|
|
173
|
+
// Add keypad, ie container with header,keys,control keys to page where specified
|
|
174
|
+
if (document.getElementById(this.targetElement)) {
|
|
175
|
+
console.log("Specified target element successfully used.");
|
|
176
|
+
document.getElementById(this.targetElement).appendChild(keypadElem);
|
|
177
|
+
} else {
|
|
178
|
+
console.log("No target element used.");
|
|
179
|
+
document.getElementsByTagName("main")[0].appendChild(keypadElem);
|
|
180
|
+
}
|
|
181
|
+
// Close connection if window closes.
|
|
182
|
+
window.onbeforeunload = () => {
|
|
183
|
+
this.conn?.close();
|
|
184
|
+
console.log("closing connection on page unload.");
|
|
185
|
+
};
|
|
186
|
+
window.onvisibilitychange = () => {
|
|
187
|
+
this.conn?.close();
|
|
188
|
+
console.log("closing connection on page unload.");
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
window.onresize = () => {
|
|
192
|
+
applyMaxKeySize(this.alphabet?.length);
|
|
193
|
+
};
|
|
194
|
+
};
|
|
195
|
+
#populateKeypad = () => {
|
|
196
|
+
const buttonResponseFn = (button) => {
|
|
197
|
+
// Send response message to experimentClient
|
|
198
|
+
const message = {
|
|
199
|
+
message: "Keypress",
|
|
200
|
+
sendId: this.peer.id,
|
|
201
|
+
recvId: this.conn.peer,
|
|
202
|
+
response: button.id,
|
|
203
|
+
timeSent: Date.now(),
|
|
204
|
+
elapsedStartToSend: Date.now() - window.startTime,
|
|
205
|
+
};
|
|
206
|
+
/**
|
|
207
|
+
* Send a signal via the peer connection to indicate keypress.
|
|
208
|
+
* This will only occur if the connection is still alive.
|
|
209
|
+
*/
|
|
210
|
+
if (this.conn && this.conn.open) {
|
|
211
|
+
this.conn.send(message); // this.conn.send(JSON.stringify(message));
|
|
212
|
+
// console.log("Keypress sent: ", JSON.stringify(message));
|
|
213
|
+
console.log("Keypress sent: ", message);
|
|
214
|
+
} else {
|
|
215
|
+
console.log("Connection is closed");
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Update keypad after a period of visible non-responsivity (ie ITI)
|
|
219
|
+
if (this.visualResponseFeedback) {
|
|
220
|
+
this.visualFeedbackThenReset(alphabet, font);
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
const createButton = (symbol) => {
|
|
224
|
+
// Create a response button for this symbol
|
|
225
|
+
let button = document.createElement("button");
|
|
226
|
+
button.id = symbol;
|
|
227
|
+
button.className = "response-button";
|
|
228
|
+
button.style.fontFamily = this.font;
|
|
229
|
+
button.style.visibility = "hidden";
|
|
230
|
+
|
|
231
|
+
const feedbackAudio = document.getElementById("feedbackAudio");
|
|
232
|
+
|
|
233
|
+
/* Set behavior for press */
|
|
234
|
+
// Sound on press...
|
|
235
|
+
button.addEventListener("touchstart", (e) => {
|
|
236
|
+
e.preventDefault();
|
|
237
|
+
console.log("touchstart event: ", e);
|
|
238
|
+
feedbackAudio.play();
|
|
239
|
+
});
|
|
240
|
+
button.addEventListener("mousedown", (e) => {
|
|
241
|
+
e.preventDefault();
|
|
242
|
+
feedbackAudio.play();
|
|
243
|
+
});
|
|
244
|
+
button.addEventListener("mouseup", (e) => {
|
|
245
|
+
e.preventDefault();
|
|
246
|
+
console.log("mouseup event: ", e);
|
|
247
|
+
switch (e.toElement.className) {
|
|
248
|
+
case "response-button-label noselect":
|
|
249
|
+
buttonResponseFn(e.toElement.parentElement); // e.target.click();
|
|
250
|
+
break;
|
|
251
|
+
case "response-button":
|
|
252
|
+
buttonResponseFn(e.toElement); // e.target.click();
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
// ...send signal on release
|
|
257
|
+
button.addEventListener("touchmove", (e) => {
|
|
258
|
+
/* prevent delay and simulated mouse events */
|
|
259
|
+
e.preventDefault();
|
|
260
|
+
});
|
|
261
|
+
button.addEventListener("touchend", (e) => {
|
|
262
|
+
/* prevent delay and simulated mouse events */
|
|
263
|
+
e.preventDefault();
|
|
264
|
+
const elementEndedOn = document.elementFromPoint(
|
|
265
|
+
e.changedTouches[0].clientX,
|
|
266
|
+
e.changedTouches[0].clientY
|
|
267
|
+
);
|
|
268
|
+
switch (elementEndedOn?.className) {
|
|
269
|
+
case "response-button-label noselect":
|
|
270
|
+
buttonResponseFn(elementEndedOn?.parentElement); // e.target.click();
|
|
271
|
+
break;
|
|
272
|
+
case "response-button":
|
|
273
|
+
buttonResponseFn(elementEndedOn); // e.target.click();
|
|
274
|
+
break;
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
// Create a label for the button
|
|
279
|
+
let buttonLabel = document.createElement("span");
|
|
280
|
+
buttonLabel.classList.add("response-button-label", "noselect");
|
|
281
|
+
buttonLabel.innerText = symbol;
|
|
282
|
+
buttonLabel.style.fontFamily = this.font;
|
|
283
|
+
|
|
284
|
+
// Add the label to the button
|
|
285
|
+
button.appendChild(buttonLabel);
|
|
286
|
+
// Add the labeled-button to the HTML
|
|
287
|
+
if (["SPACE", "RETURN"].includes(symbol.toUpperCase())) {
|
|
288
|
+
document.querySelector("#keypad-control-keys").appendChild(button);
|
|
289
|
+
} else {
|
|
290
|
+
document.querySelector("#keypad-keys").appendChild(button);
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
// Set-up an instruction/welcome message for the user
|
|
295
|
+
const header = document.getElementById("keypad-header");
|
|
296
|
+
header.innerText = this.headerMessage || "";
|
|
297
|
+
header.style.display = header.innerText === "" ? "none" : "block";
|
|
298
|
+
// Get the keypad element
|
|
299
|
+
const remoteControl = document.getElementById("keypad");
|
|
300
|
+
|
|
301
|
+
// Set-up audio element
|
|
302
|
+
const feedbackAudio = document.createElement("audio");
|
|
303
|
+
// TODO investigate why feedback audio is broken/unreliable
|
|
304
|
+
feedbackAudio.id = "feedbackAudio";
|
|
305
|
+
feedbackAudio.src = "onems.mp3";
|
|
306
|
+
header.appendChild(feedbackAudio);
|
|
307
|
+
|
|
308
|
+
// Set correct font for button labels
|
|
309
|
+
remoteControl.style.fontFamily = this.font;
|
|
310
|
+
// Remove previous buttons
|
|
311
|
+
this.clearKeys();
|
|
312
|
+
// Create new buttons
|
|
313
|
+
this.alphabet.forEach((symbol) => createButton(symbol));
|
|
314
|
+
// Manually style buttons, according to Denis' algorithm
|
|
315
|
+
setTimeout(() => applyMaxKeySize(this.alphabet.length), 5); // Why?
|
|
316
|
+
};
|
|
317
|
+
visualFeedbackThenReset = (delayTime = 800) => {
|
|
318
|
+
// ie grey out keys just after use, to discourage rapid response
|
|
319
|
+
this.interResponseKeypadMessaging();
|
|
320
|
+
// Setup keys for the next trial
|
|
321
|
+
setTimeout(defaultKeypadMessaging, delayTime);
|
|
322
|
+
};
|
|
323
|
+
defaultKeypadMessaging = (headerText = "") => {
|
|
324
|
+
// Set-up an instruction/welcome message for the user
|
|
325
|
+
const header = document.getElementById("keypad-header");
|
|
326
|
+
if (headerText === "") {
|
|
327
|
+
header.style.display = "none";
|
|
328
|
+
} else {
|
|
329
|
+
header.innerText = headerText;
|
|
330
|
+
header.style.display = "block";
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Make each button pressable
|
|
334
|
+
const buttons = document.getElementsByClassName("response-button");
|
|
335
|
+
[...buttons].forEach((element) => {
|
|
336
|
+
element.classList.remove("unpressable", "noselect");
|
|
337
|
+
});
|
|
338
|
+
};
|
|
339
|
+
interResponseKeypadMessaging = (
|
|
340
|
+
interResponseMessage = "Please fix your gaze on the + mark on your computer screen."
|
|
341
|
+
) => {
|
|
342
|
+
// Change header text to reflect WAIT message
|
|
343
|
+
const header = document.getElementById("keypad-header");
|
|
344
|
+
header.innerText = interResponseMessage;
|
|
345
|
+
|
|
346
|
+
// Then make each button unpressable
|
|
347
|
+
const buttons = document.getElementsByClassName("response-button");
|
|
348
|
+
[...buttons].forEach((element) => {
|
|
349
|
+
element.classList.add("unpressable", "noselect");
|
|
350
|
+
});
|
|
351
|
+
};
|
|
352
|
+
/**
|
|
353
|
+
* Remove all keys from the keypad.
|
|
354
|
+
*/
|
|
355
|
+
clearKeys = () => {
|
|
356
|
+
document.querySelector("#keypad-keys").innerHTML =
|
|
357
|
+
"<div id='keypad-control-keys'></div>";
|
|
358
|
+
// document.querySelector("#keypad-control-keys").innerHTML = "";
|
|
359
|
+
};
|
|
360
|
+
/**
|
|
361
|
+
* Return the nodes corresponding to the specified keys.
|
|
362
|
+
* @param {string[]} whichKeys id's of keys to select. defaults to all keys.
|
|
363
|
+
* @returns {HTMLElement[]}
|
|
364
|
+
*/
|
|
365
|
+
_getKeysElements = (whichKeys = []) => {
|
|
366
|
+
let keyElems = [
|
|
367
|
+
...document
|
|
368
|
+
.querySelector("#keypad")
|
|
369
|
+
.getElementsByClassName("response-button"),
|
|
370
|
+
];
|
|
371
|
+
if (whichKeys.length !== 0)
|
|
372
|
+
keyElems = keyElems.filter((e) => whichKeys.includes(e.id));
|
|
373
|
+
return keyElems;
|
|
374
|
+
};
|
|
375
|
+
/**
|
|
376
|
+
* Make selected keys unpressable.
|
|
377
|
+
*/
|
|
378
|
+
disableKeys = (whichKeys = []) => {
|
|
379
|
+
const keyElems = this._getKeysElements(whichKeys);
|
|
380
|
+
keyElems.forEach((e) => {
|
|
381
|
+
e.classList.add("unpressable");
|
|
382
|
+
e.classList.add("noselect");
|
|
383
|
+
e.setAttribute("inert", "");
|
|
384
|
+
});
|
|
385
|
+
};
|
|
386
|
+
/**
|
|
387
|
+
* Make selected keys pressable.
|
|
388
|
+
*/
|
|
389
|
+
enableKeys = (whichKeys = []) => {
|
|
390
|
+
const keyElems = this._getKeysElements(whichKeys);
|
|
391
|
+
keyElems.forEach((e) => {
|
|
392
|
+
e.classList.remove("unpressable");
|
|
393
|
+
e.classList.remove("noselect");
|
|
394
|
+
e.removeAttribute("inert");
|
|
395
|
+
});
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
export { Keypad };
|