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/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 = 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 };
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 };