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