virtual-keypad 5.15.2 → 5.17.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/.claude/settings.local.json +16 -0
- package/Procfile +1 -0
- package/dist/ExperimentPeer.js +2 -0
- package/dist/ExperimentPeer.js.map +1 -0
- package/dist/PhonePeer.js +2 -0
- package/dist/PhonePeer.js.map +1 -0
- package/dist/main.js +1 -1
- package/dist/main.js.map +1 -1
- package/dist/receiver.html +2 -0
- package/dist/server.js +16 -14
- package/package.json +11 -3
- package/src/ExperimentPeer.js +270 -0
- package/src/PhonePeer.js +394 -0
- package/src/__tests__/maxKeySize.test.js +80 -0
- package/src/keypad.css +35 -10
- package/src/keypad.js +19 -17
- package/src/main.js +2 -1
- package/src/maxKeySize.js +223 -48
- package/webpack.common.js +44 -16
package/src/PhonePeer.js
ADDED
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
import { applyMaxKeySize } from "./maxKeySize";
|
|
2
|
+
import "./keypad.css";
|
|
3
|
+
|
|
4
|
+
class PhonePeer {
|
|
5
|
+
constructor(
|
|
6
|
+
keypadParameters = {
|
|
7
|
+
targetElementId: null,
|
|
8
|
+
visualResponseFeedback: false,
|
|
9
|
+
}
|
|
10
|
+
) {
|
|
11
|
+
// Module identification
|
|
12
|
+
this.name = "keypad";
|
|
13
|
+
this.type = "keypad";
|
|
14
|
+
this.connectionManager = null;
|
|
15
|
+
|
|
16
|
+
console.log("targetElementId: ", keypadParameters.targetElementId);
|
|
17
|
+
|
|
18
|
+
// Keypad configuration
|
|
19
|
+
this.startTime = Date.now();
|
|
20
|
+
this.targetElement = keypadParameters.targetElementId;
|
|
21
|
+
this.controlButtons = keypadParameters.controlButtons ?? [
|
|
22
|
+
"SPACE",
|
|
23
|
+
"RETURN",
|
|
24
|
+
];
|
|
25
|
+
this.visualResponseFeedback = keypadParameters.visualResponseFeedback;
|
|
26
|
+
this.alphabet = null;
|
|
27
|
+
this.font = null;
|
|
28
|
+
this.headerMessage = "";
|
|
29
|
+
this.footerMessage = "";
|
|
30
|
+
|
|
31
|
+
// Set-up sound to play on press
|
|
32
|
+
this.pressFeedback = new Audio(this.keypressFeedbackSound);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Required method for ConnectAPeer submodule registration
|
|
36
|
+
register = (manager) => {
|
|
37
|
+
this.connectionManager = manager;
|
|
38
|
+
console.log("Keypad module registered with connection manager");
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// Required method for ConnectAPeer message handling
|
|
42
|
+
onMessage = (data, manager) => {
|
|
43
|
+
console.log("Keypad received data: ", data);
|
|
44
|
+
|
|
45
|
+
if (!data || !data.message) return;
|
|
46
|
+
|
|
47
|
+
switch (data.message) {
|
|
48
|
+
case "KeypadParameters":
|
|
49
|
+
this.alphabet = data.alphabet;
|
|
50
|
+
this.font = data.font;
|
|
51
|
+
this.controlButtons = data.controlButtons;
|
|
52
|
+
this.onErrorReconnectMessage = data.onErrorReconnectMessage;
|
|
53
|
+
this.initializeKeypad();
|
|
54
|
+
this.#populateKeypad();
|
|
55
|
+
break;
|
|
56
|
+
case "UpdateHeader":
|
|
57
|
+
document.getElementById("keypad-header").innerText = data.headerContent;
|
|
58
|
+
document.getElementById("keypad-header").style.display =
|
|
59
|
+
data.headerContent === "" ? "none" : "block";
|
|
60
|
+
this.headerMessage = data.headerContent;
|
|
61
|
+
this.#populateKeypad();
|
|
62
|
+
break;
|
|
63
|
+
case "UpdateFooter":
|
|
64
|
+
document.getElementById("keypad-footer").innerText = data.headerContent;
|
|
65
|
+
this.footerMessage = data.headerContent;
|
|
66
|
+
this.#populateKeypad();
|
|
67
|
+
break;
|
|
68
|
+
case "Update":
|
|
69
|
+
// Keypad has received data to update the keypad
|
|
70
|
+
if (
|
|
71
|
+
!data.hasOwnProperty("alphabet") &&
|
|
72
|
+
!data.hasOwnProperty("font") &&
|
|
73
|
+
!data.hasOwnProperty("controlButtons")
|
|
74
|
+
) {
|
|
75
|
+
console.error(
|
|
76
|
+
'Error in parsing data received! Must set "alphabet" or "font" properties'
|
|
77
|
+
);
|
|
78
|
+
} else {
|
|
79
|
+
this.alphabet = data.alphabet ?? this.alphabet;
|
|
80
|
+
this.font = data.font ?? this.font;
|
|
81
|
+
this.controlButtons = data.controlButtons ?? this.controlButtons;
|
|
82
|
+
}
|
|
83
|
+
this.#populateKeypad();
|
|
84
|
+
break;
|
|
85
|
+
case "Disable":
|
|
86
|
+
if (data.hasOwnProperty("keys")) {
|
|
87
|
+
this.disableKeys(data.keys);
|
|
88
|
+
} else {
|
|
89
|
+
this.disableKeys();
|
|
90
|
+
}
|
|
91
|
+
break;
|
|
92
|
+
case "Enable":
|
|
93
|
+
if (data.hasOwnProperty("keys")) {
|
|
94
|
+
this.enableKeys(data.keys);
|
|
95
|
+
} else {
|
|
96
|
+
this.enableKeys();
|
|
97
|
+
}
|
|
98
|
+
break;
|
|
99
|
+
case "Heartbeat":
|
|
100
|
+
this.lastHeartbeat = performance.now();
|
|
101
|
+
break;
|
|
102
|
+
case "InitializeKeypad":
|
|
103
|
+
this.initializeKeypad();
|
|
104
|
+
break;
|
|
105
|
+
default:
|
|
106
|
+
console.log("Unhandled message type: ", data.message);
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
#prepareHTML = () => {
|
|
111
|
+
/**
|
|
112
|
+
* ----------
|
|
113
|
+
* | Header |
|
|
114
|
+
* ----------
|
|
115
|
+
* |a b c d | <- keypad-keys
|
|
116
|
+
* |e f g h |
|
|
117
|
+
* |i j k l |
|
|
118
|
+
* ----------
|
|
119
|
+
* |space ret| <- keypad-control-keys
|
|
120
|
+
*/
|
|
121
|
+
// Keypad elem is a container for a message and all keys
|
|
122
|
+
const keypadElem = document.createElement("div");
|
|
123
|
+
keypadElem.setAttribute("id", "keypad");
|
|
124
|
+
const keypadHeader = document.createElement("h1");
|
|
125
|
+
keypadHeader.setAttribute("id", "keypad-header");
|
|
126
|
+
keypadHeader.classList.add("noselect");
|
|
127
|
+
const keypadKeys = document.createElement("div");
|
|
128
|
+
keypadKeys.setAttribute("id", "keypad-keys");
|
|
129
|
+
keypadKeys.classList.add("keys");
|
|
130
|
+
const keypadControlKeys = document.createElement("div");
|
|
131
|
+
keypadControlKeys.setAttribute("id", "keypad-control-keys");
|
|
132
|
+
keypadControlKeys.classList.add("keys");
|
|
133
|
+
const keypadFooter = document.createElement("div");
|
|
134
|
+
keypadFooter.setAttribute("id", "keypad-footer");
|
|
135
|
+
keypadElem.appendChild(keypadHeader);
|
|
136
|
+
keypadKeys.appendChild(keypadControlKeys);
|
|
137
|
+
keypadElem.appendChild(keypadKeys);
|
|
138
|
+
keypadElem.appendChild(keypadFooter);
|
|
139
|
+
|
|
140
|
+
// Add keypad to page where specified
|
|
141
|
+
if (document.getElementById(this.targetElement)) {
|
|
142
|
+
console.log("Specified target element successfully used.");
|
|
143
|
+
const targetElement = document.getElementById(this.targetElement);
|
|
144
|
+
targetElement.innerHTML = "";
|
|
145
|
+
targetElement.appendChild(keypadElem);
|
|
146
|
+
} else {
|
|
147
|
+
console.log("No target element used.");
|
|
148
|
+
document.getElementsByTagName("main")[0].appendChild(keypadElem);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
window.onresize = () => {
|
|
152
|
+
console.count("Window resized.");
|
|
153
|
+
applyMaxKeySize(this.alphabet?.length);
|
|
154
|
+
};
|
|
155
|
+
if (window.visualViewport)
|
|
156
|
+
window.visualViewport.onresize = () => {
|
|
157
|
+
console.count("VisualViewport resized.");
|
|
158
|
+
applyMaxKeySize(this.alphabet?.length);
|
|
159
|
+
};
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
#populateKeypad = () => {
|
|
163
|
+
const buttonResponseFn = (button) => {
|
|
164
|
+
// Send response message to experimentClient
|
|
165
|
+
const message = {
|
|
166
|
+
type: "keypad", // Add type for ConnectAPeer routing
|
|
167
|
+
message: "Keypress",
|
|
168
|
+
response: button.id,
|
|
169
|
+
timeSent: Date.now(),
|
|
170
|
+
elapsedStartToSend: Date.now() - window.startTime,
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
// Send the keypress through the connection manager
|
|
174
|
+
if (this.connectionManager) {
|
|
175
|
+
this.connectionManager.send(message);
|
|
176
|
+
console.log("Keypress sent: ", message);
|
|
177
|
+
} else {
|
|
178
|
+
console.log("Connection manager not available");
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Update keypad after a period of visible non-responsivity (ie ITI)
|
|
182
|
+
if (this.visualResponseFeedback) {
|
|
183
|
+
this.visualFeedbackThenReset(this.alphabet, this.font);
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const createButton = (symbol) => {
|
|
188
|
+
// Create a response button for this symbol
|
|
189
|
+
let button = document.createElement("button");
|
|
190
|
+
button.id = symbol;
|
|
191
|
+
button.className = "response-button";
|
|
192
|
+
button.style.fontFamily = this.font;
|
|
193
|
+
button.style.visibility = "hidden";
|
|
194
|
+
|
|
195
|
+
const feedbackAudio = document.getElementById("feedbackAudio");
|
|
196
|
+
|
|
197
|
+
/* Set behavior for press */
|
|
198
|
+
// Sound on press...
|
|
199
|
+
button.addEventListener("touchstart", (e) => {
|
|
200
|
+
e.preventDefault();
|
|
201
|
+
console.log("touchstart event: ", e);
|
|
202
|
+
feedbackAudio.play();
|
|
203
|
+
});
|
|
204
|
+
button.addEventListener("mousedown", (e) => {
|
|
205
|
+
e.preventDefault();
|
|
206
|
+
feedbackAudio.play();
|
|
207
|
+
});
|
|
208
|
+
button.addEventListener("mouseup", (e) => {
|
|
209
|
+
e.preventDefault();
|
|
210
|
+
console.log("mouseup event: ", e);
|
|
211
|
+
switch (e.target.className) {
|
|
212
|
+
case "response-button-label noselect":
|
|
213
|
+
buttonResponseFn(e.target.parentElement);
|
|
214
|
+
break;
|
|
215
|
+
case "response-button":
|
|
216
|
+
buttonResponseFn(e.target);
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
// ...send signal on release
|
|
221
|
+
button.addEventListener("touchmove", (e) => {
|
|
222
|
+
/* prevent delay and simulated mouse events */
|
|
223
|
+
e.preventDefault();
|
|
224
|
+
});
|
|
225
|
+
button.addEventListener("touchend", (e) => {
|
|
226
|
+
/* prevent delay and simulated mouse events */
|
|
227
|
+
e.preventDefault();
|
|
228
|
+
const elementEndedOn = document.elementFromPoint(
|
|
229
|
+
e.changedTouches[0].clientX,
|
|
230
|
+
e.changedTouches[0].clientY
|
|
231
|
+
);
|
|
232
|
+
switch (elementEndedOn?.className) {
|
|
233
|
+
case "response-button-label noselect":
|
|
234
|
+
buttonResponseFn(elementEndedOn?.parentElement);
|
|
235
|
+
break;
|
|
236
|
+
case "response-button":
|
|
237
|
+
buttonResponseFn(elementEndedOn);
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
// Create a label for the button
|
|
243
|
+
let buttonLabel = document.createElement("span");
|
|
244
|
+
buttonLabel.classList.add("response-button-label", "noselect");
|
|
245
|
+
buttonLabel.innerText = symbol;
|
|
246
|
+
buttonLabel.style.fontFamily = this.font;
|
|
247
|
+
|
|
248
|
+
// Add the label to the button
|
|
249
|
+
button.appendChild(buttonLabel);
|
|
250
|
+
// Add the labeled-button to the HTML
|
|
251
|
+
if (
|
|
252
|
+
this.controlButtons
|
|
253
|
+
.map((x) => x.toLowerCase())
|
|
254
|
+
.includes(symbol.toLowerCase())
|
|
255
|
+
) {
|
|
256
|
+
document.querySelector("#keypad-control-keys").appendChild(button);
|
|
257
|
+
} else {
|
|
258
|
+
document.querySelector("#keypad-keys").appendChild(button);
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
// Set-up an instruction/welcome message for the user
|
|
263
|
+
const header = document.getElementById("keypad-header");
|
|
264
|
+
header.innerText = this.headerMessage || "";
|
|
265
|
+
header.style.display = header.innerText === "" ? "none" : "block";
|
|
266
|
+
// Get the keypad element
|
|
267
|
+
const remoteControl = document.getElementById("keypad");
|
|
268
|
+
|
|
269
|
+
// Set-up audio element
|
|
270
|
+
const feedbackAudio = document.createElement("audio");
|
|
271
|
+
feedbackAudio.id = "feedbackAudio";
|
|
272
|
+
feedbackAudio.src = "onems.mp3";
|
|
273
|
+
header.appendChild(feedbackAudio);
|
|
274
|
+
|
|
275
|
+
// Set correct font for button labels
|
|
276
|
+
remoteControl.style.fontFamily = this.font;
|
|
277
|
+
// Remove previous buttons
|
|
278
|
+
this.clearKeys();
|
|
279
|
+
// Create new buttons
|
|
280
|
+
if (this.alphabet) {
|
|
281
|
+
this.alphabet.forEach((symbol) => createButton(symbol));
|
|
282
|
+
// Manually style buttons, according to Denis' algorithm
|
|
283
|
+
setTimeout(() => applyMaxKeySize(this.alphabet.length), 5);
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
visualFeedbackThenReset = (delayTime = 800) => {
|
|
288
|
+
// ie grey out keys just after use, to discourage rapid response
|
|
289
|
+
this.interResponseKeypadMessaging();
|
|
290
|
+
// Setup keys for the next trial
|
|
291
|
+
setTimeout(() => this.defaultKeypadMessaging(), delayTime);
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
defaultKeypadMessaging = (headerText = "") => {
|
|
295
|
+
// Set-up an instruction/welcome message for the user
|
|
296
|
+
const header = document.getElementById("keypad-header");
|
|
297
|
+
if (headerText === "") {
|
|
298
|
+
header.style.display = "none";
|
|
299
|
+
} else {
|
|
300
|
+
header.innerText = headerText;
|
|
301
|
+
header.style.display = "block";
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Make each button pressable
|
|
305
|
+
const buttons = document.getElementsByClassName("response-button");
|
|
306
|
+
[...buttons].forEach((element) => {
|
|
307
|
+
element.classList.remove("unpressable", "noselect");
|
|
308
|
+
});
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
interResponseKeypadMessaging = (
|
|
312
|
+
interResponseMessage = "Please fix your gaze on the + mark on your computer screen."
|
|
313
|
+
) => {
|
|
314
|
+
// Change header text to reflect WAIT message
|
|
315
|
+
const header = document.getElementById("keypad-header");
|
|
316
|
+
header.innerText = interResponseMessage;
|
|
317
|
+
|
|
318
|
+
// Then make each button unpressable
|
|
319
|
+
const buttons = document.getElementsByClassName("response-button");
|
|
320
|
+
[...buttons].forEach((element) => {
|
|
321
|
+
element.classList.add("unpressable", "noselect");
|
|
322
|
+
});
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Remove all keys from the keypad.
|
|
327
|
+
*/
|
|
328
|
+
clearKeys = () => {
|
|
329
|
+
document.querySelector("#keypad-keys").innerHTML =
|
|
330
|
+
"<div id='keypad-control-keys'></div>";
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Return the nodes corresponding to the specified keys.
|
|
335
|
+
* @param {string[]} whichKeys id's of keys to select. defaults to all keys.
|
|
336
|
+
* @returns {HTMLElement[]}
|
|
337
|
+
*/
|
|
338
|
+
_getKeysElements = (whichKeys = []) => {
|
|
339
|
+
let keyElems = [
|
|
340
|
+
...document
|
|
341
|
+
.querySelector("#keypad")
|
|
342
|
+
.getElementsByClassName("response-button"),
|
|
343
|
+
];
|
|
344
|
+
if (whichKeys.length !== 0)
|
|
345
|
+
keyElems = keyElems.filter((e) => whichKeys.includes(e.id));
|
|
346
|
+
return keyElems;
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Make selected keys unpressable.
|
|
351
|
+
*/
|
|
352
|
+
disableKeys = (whichKeys = []) => {
|
|
353
|
+
const keyElems = this._getKeysElements(whichKeys);
|
|
354
|
+
keyElems.forEach((e) => {
|
|
355
|
+
e.classList.add("unpressable");
|
|
356
|
+
e.classList.add("noselect");
|
|
357
|
+
e.setAttribute("inert", "");
|
|
358
|
+
});
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Make selected keys pressable.
|
|
363
|
+
*/
|
|
364
|
+
enableKeys = (whichKeys = []) => {
|
|
365
|
+
const keyElems = this._getKeysElements(whichKeys);
|
|
366
|
+
keyElems.forEach((e) => {
|
|
367
|
+
e.classList.remove("unpressable");
|
|
368
|
+
e.classList.remove("noselect");
|
|
369
|
+
e.removeAttribute("inert");
|
|
370
|
+
});
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Initialize the keypad DOM elements and set up event listeners.
|
|
375
|
+
* Call this method when you're ready to display the keypad.
|
|
376
|
+
*/
|
|
377
|
+
initializeKeypad() {
|
|
378
|
+
this.#prepareHTML();
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
async function main() {
|
|
383
|
+
const pp = new PhonePeer({
|
|
384
|
+
targetElementId: "target",
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
if (window.phoneApp) {
|
|
388
|
+
window.phoneApp.registerSubmodule(pp);
|
|
389
|
+
} else {
|
|
390
|
+
console.log("PhoneApp not found");
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
main().catch((err) => console.error(err));
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { applyMaxKeySize } from '../maxKeySize';
|
|
2
|
+
|
|
3
|
+
describe('maxKeySize', () => {
|
|
4
|
+
beforeEach(() => {
|
|
5
|
+
// Setup DOM structure
|
|
6
|
+
document.body.innerHTML = `
|
|
7
|
+
<div id="keypad">
|
|
8
|
+
<div id="keypad-keys">
|
|
9
|
+
<div id="keypad-control-keys"></div>
|
|
10
|
+
</div>
|
|
11
|
+
</div>
|
|
12
|
+
`;
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test('applies same font size to control buttons with long labels', () => {
|
|
16
|
+
// Add control buttons
|
|
17
|
+
const controlKeys = ['SKIP BLOCK', 'VERY LONG LABEL THAT WRAPS', 'RETURN'];
|
|
18
|
+
controlKeys.forEach((label) => {
|
|
19
|
+
const btn = document.createElement('button');
|
|
20
|
+
btn.className = 'response-button';
|
|
21
|
+
btn.innerHTML = `<span class="response-button-label">${label}</span>`;
|
|
22
|
+
document.getElementById('keypad-control-keys').appendChild(btn);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// Add regular keys
|
|
26
|
+
for (let i = 0; i < 8; i++) {
|
|
27
|
+
const btn = document.createElement('button');
|
|
28
|
+
btn.className = 'response-button';
|
|
29
|
+
btn.innerHTML = `<span class="response-button-label">${String.fromCharCode(65 + i)}</span>`;
|
|
30
|
+
document.getElementById('keypad-keys').appendChild(btn);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Apply font sizing
|
|
34
|
+
applyMaxKeySize(8 + 3, 'sans-serif');
|
|
35
|
+
|
|
36
|
+
// Check that control buttons have same font size
|
|
37
|
+
const controlButtons = document.querySelectorAll('#keypad-control-keys .response-button');
|
|
38
|
+
const firstFontSize = parseFloat(controlButtons[0].style.fontSize);
|
|
39
|
+
|
|
40
|
+
controlButtons.forEach((btn) => {
|
|
41
|
+
expect(parseFloat(btn.style.fontSize)).toBe(firstFontSize);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// Check that font size is reasonable
|
|
45
|
+
expect(firstFontSize).toBeGreaterThan(8);
|
|
46
|
+
expect(firstFontSize).toBeLessThan(50);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('handles CJK characters in control button labels', () => {
|
|
50
|
+
// Add control buttons
|
|
51
|
+
const controlKeys = ['確認', '確認', '確認']; // Japanese/Chinese
|
|
52
|
+
controlKeys.forEach((label) => {
|
|
53
|
+
const btn = document.createElement('button');
|
|
54
|
+
btn.className = 'response-button';
|
|
55
|
+
btn.innerHTML = `<span class="response-button-label">${label}</span>`;
|
|
56
|
+
document.getElementById('keypad-control-keys').appendChild(btn);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// Add regular keys
|
|
60
|
+
for (let i = 0; i < 8; i++) {
|
|
61
|
+
const btn = document.createElement('button');
|
|
62
|
+
btn.className = 'response-button';
|
|
63
|
+
btn.innerHTML = `<span class="response-button-label">${String.fromCharCode(65 + i)}</span>`;
|
|
64
|
+
document.getElementById('keypad-keys').appendChild(btn);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Apply font sizing
|
|
68
|
+
applyMaxKeySize(8 + 3, 'sans-serif');
|
|
69
|
+
|
|
70
|
+
const controlButtons = document.querySelectorAll('#keypad-control-keys .response-button');
|
|
71
|
+
controlButtons.forEach((btn) => {
|
|
72
|
+
const fontSize = parseFloat(btn.style.fontSize);
|
|
73
|
+
expect(fontSize).toBeGreaterThan(10);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
afterEach(() => {
|
|
78
|
+
document.body.innerHTML = '';
|
|
79
|
+
});
|
|
80
|
+
});
|
package/src/keypad.css
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
display: grid;
|
|
9
9
|
grid-template-columns: 1fr;
|
|
10
|
-
/* Make header text as big as
|
|
10
|
+
/* Make header text as big as possible, keys take up as much space as needed, and control keys take up bottom space */
|
|
11
11
|
grid-template-rows: max-content auto;
|
|
12
12
|
|
|
13
13
|
align-items: center;
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
font-family: sans-serif;
|
|
17
17
|
}
|
|
18
|
+
|
|
18
19
|
#keypad-header {
|
|
19
20
|
height: 100%;
|
|
20
21
|
font-size: 1.5rem;
|
|
@@ -29,6 +30,7 @@
|
|
|
29
30
|
box-shadow: inset 2px 2px 4px #888888,
|
|
30
31
|
inset -2px -2px 4px #fff;
|
|
31
32
|
}
|
|
33
|
+
|
|
32
34
|
#keypad-footer {
|
|
33
35
|
position: absolute;
|
|
34
36
|
font-size: smaller;
|
|
@@ -39,13 +41,9 @@
|
|
|
39
41
|
|
|
40
42
|
/* Grid of main keys themselves */
|
|
41
43
|
#keypad-keys {
|
|
42
|
-
/* Second row, within
|
|
44
|
+
/* Second row, within keypad grid (i.e., whole page) */
|
|
43
45
|
grid-row: 2;
|
|
44
46
|
}
|
|
45
|
-
/* Hide scrollbar */
|
|
46
|
-
/* #keypad-keys::-webkit-scrollbar {
|
|
47
|
-
display: none;
|
|
48
|
-
} */
|
|
49
47
|
|
|
50
48
|
.response-button {
|
|
51
49
|
/* border: 1px solid red; */
|
|
@@ -55,6 +53,7 @@
|
|
|
55
53
|
display: flex;
|
|
56
54
|
align-items: center;
|
|
57
55
|
justify-content: center;
|
|
56
|
+
flex-direction: column; /* NEW: Enable vertical layout for wrapping */
|
|
58
57
|
|
|
59
58
|
/* Curve corners of buttons */
|
|
60
59
|
border-radius: min(25px, 15%);
|
|
@@ -69,13 +68,18 @@
|
|
|
69
68
|
|
|
70
69
|
box-shadow: 1px 1px 2px #888888,
|
|
71
70
|
-1px -1px 2px #fff;
|
|
71
|
+
|
|
72
|
+
/* NEW: Enable flexible text layout */
|
|
73
|
+
overflow: hidden;
|
|
74
|
+
box-sizing: border-box;
|
|
72
75
|
}
|
|
76
|
+
|
|
73
77
|
.response-button:active {
|
|
74
78
|
box-shadow: 1px 2px 2px 1px #999;
|
|
75
79
|
background-color: #aaa;
|
|
76
80
|
}
|
|
77
81
|
|
|
78
|
-
/* Only do on a hover-enabled device,
|
|
82
|
+
/* Only do on a hover-enabled device, i.e., not a phone/tablet */
|
|
79
83
|
@media (hover: hover) {
|
|
80
84
|
.response-button:hover {
|
|
81
85
|
box-shadow: inset 1px 1px 2px #888888,
|
|
@@ -83,10 +87,22 @@
|
|
|
83
87
|
}
|
|
84
88
|
}
|
|
85
89
|
|
|
86
|
-
|
|
87
90
|
.response-button-label {
|
|
88
91
|
text-align: center;
|
|
89
|
-
|
|
92
|
+
color: black;
|
|
93
|
+
|
|
94
|
+
/* NEW: Enable text wrapping and centering */
|
|
95
|
+
display: flex;
|
|
96
|
+
align-items: center;
|
|
97
|
+
justify-content: center;
|
|
98
|
+
width: 100%;
|
|
99
|
+
height: 100%;
|
|
100
|
+
white-space: pre-wrap;
|
|
101
|
+
word-wrap: break-word;
|
|
102
|
+
word-break: normal;
|
|
103
|
+
overflow: hidden;
|
|
104
|
+
padding: 2px;
|
|
105
|
+
box-sizing: border-box;
|
|
90
106
|
}
|
|
91
107
|
|
|
92
108
|
.noselect {
|
|
@@ -106,4 +122,13 @@
|
|
|
106
122
|
inset -1px -1px 2px #fff;
|
|
107
123
|
pointer-events: none;
|
|
108
124
|
user-select: none;
|
|
109
|
-
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/* Control buttons maintain bottom alignment */
|
|
128
|
+
#keypad-control-keys .response-button {
|
|
129
|
+
border-radius: 25px;
|
|
130
|
+
display: flex;
|
|
131
|
+
align-items: center;
|
|
132
|
+
justify-content: center;
|
|
133
|
+
flex-direction: column; /* NEW: Enable vertical layout */
|
|
134
|
+
}
|
package/src/keypad.js
CHANGED
|
@@ -82,7 +82,7 @@ class Keypad extends KeypadPeer {
|
|
|
82
82
|
this.#populateKeypad();
|
|
83
83
|
break;
|
|
84
84
|
case "Update":
|
|
85
|
-
// Keypad has received data to update
|
|
85
|
+
// Keypad has received data to update keypad
|
|
86
86
|
if (!data.hasOwnProperty("alphabet") && !data.hasOwnProperty("font") && !data.hasOwnProperty("controlButtons")) {
|
|
87
87
|
console.error(
|
|
88
88
|
'Error in parsing data received! Must set "alphabet" or "font" properties',
|
|
@@ -120,7 +120,7 @@ class Keypad extends KeypadPeer {
|
|
|
120
120
|
};
|
|
121
121
|
#join = () => {
|
|
122
122
|
/**
|
|
123
|
-
* Create
|
|
123
|
+
* Create connection between two Peers.
|
|
124
124
|
*
|
|
125
125
|
* Sets up callbacks that handle any events related to the
|
|
126
126
|
* connection and data received on it.
|
|
@@ -129,14 +129,14 @@ class Keypad extends KeypadPeer {
|
|
|
129
129
|
if (this.conn) {
|
|
130
130
|
this.conn.close();
|
|
131
131
|
}
|
|
132
|
-
// Create connection to destination peer specified by
|
|
132
|
+
// Create connection to destination peer specified by query param
|
|
133
133
|
this.conn = this.peer.connect(this.receiverPeerId, {
|
|
134
134
|
reliable: true,
|
|
135
135
|
});
|
|
136
136
|
|
|
137
137
|
console.log("Connection: ", this.conn);
|
|
138
138
|
this.conn.on("open", this.#initiateHandshake);
|
|
139
|
-
// Handle incoming data (messages only since this is
|
|
139
|
+
// Handle incoming data (messages only since this is signal sender)
|
|
140
140
|
this.conn.on("data", this.#onConnData);
|
|
141
141
|
// TODO figure out how to re-establish connection, or have more robust connection
|
|
142
142
|
this.conn.on("close", () => console.log("Connection closed"));
|
|
@@ -194,11 +194,13 @@ class Keypad extends KeypadPeer {
|
|
|
194
194
|
|
|
195
195
|
window.onresize = () => {
|
|
196
196
|
console.count("Window resized.");
|
|
197
|
-
applyMaxKeySize
|
|
197
|
+
// NEW: Pass font family to applyMaxKeySize
|
|
198
|
+
applyMaxKeySize(this.alphabet?.length, this.font);
|
|
198
199
|
};
|
|
199
200
|
if (window.visualViewport) window.visualViewport.onresize = () => {
|
|
200
201
|
console.count("VisualViewport resized.");
|
|
201
|
-
applyMaxKeySize
|
|
202
|
+
// NEW: Pass font family to applyMaxKeySize
|
|
203
|
+
applyMaxKeySize(this.alphabet?.length, this.font);
|
|
202
204
|
};
|
|
203
205
|
};
|
|
204
206
|
#populateKeypad = () => {
|
|
@@ -284,15 +286,15 @@ class Keypad extends KeypadPeer {
|
|
|
284
286
|
}
|
|
285
287
|
});
|
|
286
288
|
|
|
287
|
-
// Create a label for
|
|
289
|
+
// Create a label for button
|
|
288
290
|
let buttonLabel = document.createElement("span");
|
|
289
291
|
buttonLabel.classList.add("response-button-label", "noselect");
|
|
290
292
|
buttonLabel.innerText = symbol;
|
|
291
293
|
buttonLabel.style.fontFamily = this.font;
|
|
292
294
|
|
|
293
|
-
// Add
|
|
295
|
+
// Add to label to button
|
|
294
296
|
button.appendChild(buttonLabel);
|
|
295
|
-
// Add
|
|
297
|
+
// Add to labeled-button to HTML
|
|
296
298
|
if (this.controlButtons.map(x => x.toLowerCase()).includes(symbol.toLowerCase())) {
|
|
297
299
|
document.querySelector("#keypad-control-keys").appendChild(button);
|
|
298
300
|
} else {
|
|
@@ -300,11 +302,11 @@ class Keypad extends KeypadPeer {
|
|
|
300
302
|
}
|
|
301
303
|
};
|
|
302
304
|
|
|
303
|
-
// Set-up an instruction/welcome message for
|
|
305
|
+
// Set-up an instruction/welcome message for user
|
|
304
306
|
const header = document.getElementById("keypad-header");
|
|
305
307
|
header.innerText = this.headerMessage || "";
|
|
306
308
|
header.style.display = header.innerText === "" ? "none" : "block";
|
|
307
|
-
// Get
|
|
309
|
+
// Get keypad element
|
|
308
310
|
const remoteControl = document.getElementById("keypad");
|
|
309
311
|
|
|
310
312
|
// Set-up audio element
|
|
@@ -321,16 +323,17 @@ class Keypad extends KeypadPeer {
|
|
|
321
323
|
// Create new buttons
|
|
322
324
|
this.alphabet.forEach((symbol) => createButton(symbol));
|
|
323
325
|
// Manually style buttons, according to Denis' algorithm
|
|
324
|
-
|
|
326
|
+
// NEW: Pass font family to applyMaxKeySize
|
|
327
|
+
setTimeout(() => applyMaxKeySize(this.alphabet?.length, this.font), 5); // Why?
|
|
325
328
|
};
|
|
326
329
|
visualFeedbackThenReset = (delayTime = 800) => {
|
|
327
330
|
// ie grey out keys just after use, to discourage rapid response
|
|
328
331
|
this.interResponseKeypadMessaging();
|
|
329
|
-
// Setup keys for
|
|
332
|
+
// Setup keys for next trial
|
|
330
333
|
setTimeout(defaultKeypadMessaging, delayTime);
|
|
331
334
|
};
|
|
332
335
|
defaultKeypadMessaging = (headerText = "") => {
|
|
333
|
-
// Set-up an instruction/welcome message for
|
|
336
|
+
// Set-up an instruction/welcome message for user
|
|
334
337
|
const header = document.getElementById("keypad-header");
|
|
335
338
|
if (headerText === "") {
|
|
336
339
|
header.style.display = "none";
|
|
@@ -359,15 +362,14 @@ class Keypad extends KeypadPeer {
|
|
|
359
362
|
});
|
|
360
363
|
};
|
|
361
364
|
/**
|
|
362
|
-
* Remove all keys from
|
|
365
|
+
* Remove all keys from keypad.
|
|
363
366
|
*/
|
|
364
367
|
clearKeys = () => {
|
|
365
368
|
document.querySelector("#keypad-keys").innerHTML =
|
|
366
369
|
"<div id='keypad-control-keys'></div>";
|
|
367
|
-
// document.querySelector("#keypad-control-keys").innerHTML = "";
|
|
368
370
|
};
|
|
369
371
|
/**
|
|
370
|
-
* Return
|
|
372
|
+
* Return nodes corresponding to specified keys.
|
|
371
373
|
* @param {string[]} whichKeys id's of keys to select. defaults to all keys.
|
|
372
374
|
* @returns {HTMLElement[]}
|
|
373
375
|
*/
|