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