virtual-keypad 5.9.0 → 5.10.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "virtual-keypad",
3
- "version": "5.9.0",
3
+ "version": "5.10.0",
4
4
  "description": "User response at a distance. Simple utility to summon forth a virtual keypad webapp.",
5
5
  "main": "dist/main.js",
6
6
  "directories": {
package/src/keypad.css CHANGED
@@ -1,5 +1,6 @@
1
1
  /* REMOTE KEYPAD STYLING */
2
2
  #keypad {
3
+ position: absolute;
3
4
  width: 100%;
4
5
  height: 100vh;
5
6
  height: 100dvh;
@@ -7,12 +8,11 @@
7
8
  display: grid;
8
9
  grid-template-columns: 1fr;
9
10
  /* Make header text as big as the text, keys take up as much space as needed, and control keys take up bottom space */
10
- grid-template-rows: max-content auto 15vh;
11
+ grid-template-rows: max-content auto;
11
12
 
12
13
  align-items: center;
13
14
  justify-content: center;
14
15
 
15
- padding: 1em;
16
16
  font-family: sans-serif;
17
17
  }
18
18
  #keypad-header {
@@ -50,34 +50,38 @@
50
50
 
51
51
  padding: 10px;
52
52
 
53
- overflow-y: auto;
54
- overflow-x: visible;
53
+ margin: 15px;
55
54
 
56
- width: 100%;
55
+ /* overflow-y: auto;
56
+ overflow-x: visible; */
57
+
58
+ /* width: 100%;
57
59
  height: 100%;
58
60
  display: flex;
59
61
  flex-wrap: wrap-reverse;
60
62
  flex-direction: row;
61
63
  column-gap: 5px;
62
- row-gap: 5px;
64
+ row-gap: 5px; */
63
65
 
64
66
  /* Horizontal shift of buttons */
65
- justify-content: center;
67
+ /* justify-content: center; */
66
68
 
67
69
  /* Vertical shift of buttons */
68
- align-items: center;
70
+ /* align-items: center; */
69
71
 
70
72
  /* Hide scrollbar */
71
- -ms-overflow-style: none;
72
- scrollbar-width: none;
73
+ /* -ms-overflow-style: none;
74
+ scrollbar-width: none; */
73
75
  }
74
- #keypad-keys::-webkit-scrollbar {
75
- /* Hide scrollbar */
76
+ /* Hide scrollbar */
77
+ /* #keypad-keys::-webkit-scrollbar {
76
78
  display: none;
77
- }
79
+ } */
78
80
 
79
81
  .response-button {
80
- width: 100%;
82
+ cursor: pointer;
83
+
84
+ /* width: 100%;
81
85
  height: 100%;
82
86
  max-width: min(25vw, 25vh, 150px);
83
87
  max-height: min(25vw, 25vh, 150px);
@@ -101,46 +105,46 @@
101
105
  font-weight: 700;
102
106
 
103
107
 
104
- box-shadow: 7px 7px 11px #888888,
105
- -7px -7px 11px #fff;
108
+ box-shadow: 1px 1px 2px #888888,
109
+ -1px -1px 2px #fff;
106
110
  }
107
111
  .response-button:active {
108
- box-shadow: 1px 5px 5px 1px #999;
112
+ box-shadow: 1px 2px 2px 1px #999;
109
113
  background-color: #aaa;
110
114
  }
111
115
 
112
- #keypad-control-keys {
116
+ /* #keypad-control-keys {
113
117
  grid-row: 3;
114
118
 
115
119
  display: grid;
116
120
  grid-template-columns: repeat(2, 1fr);
117
121
  align-items: center;
118
122
  justify-content: center;
119
- }
120
- #keypad-control-keys > button {
123
+ } */
124
+ /* #keypad-control-keys > button {
121
125
  height: 95%;
122
126
  width: 95%;
123
127
  min-width: 95%;
124
128
  max-height: min(15vh, 150px);
125
- }
129
+ } */
126
130
 
127
131
  .response-button#SPACE {
128
132
  /* TODO actually make sure test doesn't clip */
129
- font-size: 1.5rem;
130
- grid-column: 1;
131
- grid-row: 1;
133
+ font-size: larger;
134
+ /* grid-column: 1;
135
+ grid-row: 1; */
132
136
  }
133
137
  .response-button#RETURN {
134
138
  /* TODO actually make sure test doesn't clip */
135
- font-size: 1.5rem;
136
- grid-column: 2;
137
- grid-row: 1;
139
+ font-size: larger;
140
+ /* grid-column: 2;
141
+ grid-row: 1; */
138
142
  }
139
143
  /* Only do on a hover-enabled device, ie not a phone/tablet */
140
144
  @media (hover: hover) {
141
145
  .response-button:hover {
142
- box-shadow: inset 7px 7px 11px #888888,
143
- inset -7px -7px 11px #fff;
146
+ box-shadow: inset 1px 1px 2px #888888,
147
+ inset -1px -1px 2px #fff;
144
148
  }
145
149
  }
146
150
 
@@ -167,8 +171,8 @@
167
171
 
168
172
  .unpressable {
169
173
  background-color: #aaaaaa;
170
- box-shadow: inset 7px 7px 11px #888888,
171
- inset -7px -7px 11px #fff;
174
+ box-shadow: inset 1px 1px 2px #888888,
175
+ inset -1px -1px 2px #fff;
172
176
  pointer-events: none;
173
177
  user-select: none;
174
178
  }
package/src/keypad.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { applyMaxKeySize } from "./maxKeySize";
1
2
  import "./keypad.css";
2
3
  import { KeypadPeer, keypadUrl } from "./keypadPeer.js";
3
4
 
@@ -67,6 +68,7 @@ class Keypad extends KeypadPeer {
67
68
  break;
68
69
  case "UpdateHeader":
69
70
  document.getElementById("keypad-header").innerText = data.headerContent;
71
+ document.getElementById("keypad-header").style.display = data.headerContent === "" ? "none" : "block";
70
72
  this.headerMessage = data.headerContent;
71
73
  break;
72
74
  case "UpdateFooter":
@@ -99,6 +101,10 @@ class Keypad extends KeypadPeer {
99
101
  this.enableKeys();
100
102
  }
101
103
  break;
104
+ // TODO factor out into keypadPeer
105
+ case "Heartbeat":
106
+ this.lastHeartbeat = performance.now();
107
+ break;
102
108
  default:
103
109
  console.log("Message type: ", data.message);
104
110
  }
@@ -127,7 +133,8 @@ class Keypad extends KeypadPeer {
127
133
  this.conn.on("close", () => console.log("Connection closed") );
128
134
  };
129
135
  #initiateHandshake = () => {
130
- this.conn.send({ message: "Handshake", })
136
+ this.conn.send({ message: "Handshake" });
137
+ this._setupHeartBeatIntervals();
131
138
  };
132
139
  #prepareHTML = () => {
133
140
  /**
@@ -155,6 +162,7 @@ class Keypad extends KeypadPeer {
155
162
  const keypadFooter = document.createElement("div");
156
163
  keypadFooter.setAttribute("id", "keypad-footer");
157
164
  keypadElem.appendChild(keypadHeader);
165
+ keypadKeys.appendChild(keypadControlKeys);
158
166
  keypadElem.appendChild(keypadKeys);
159
167
  keypadElem.appendChild(keypadControlKeys);
160
168
  keypadElem.appendChild(keypadFooter);
@@ -169,6 +177,8 @@ class Keypad extends KeypadPeer {
169
177
  // Close connection if window closes.
170
178
  window.onbeforeunload = () => {this.conn?.close(); console.log("closing connection on page unload.")};
171
179
  window.onvisibilitychange = () => {this.conn?.close(); console.log("closing connection on page unload.")};
180
+
181
+ window.onresize = () => {const resize = () => applyMaxKeySize(this.alphabet?.length); setTimeout(resize, 10); console.log("Repositioning buttons due to resize.")};
172
182
  };
173
183
  #populateKeypad = () => {
174
184
  const buttonResponseFn = (button) => {
@@ -200,7 +210,6 @@ class Keypad extends KeypadPeer {
200
210
  };
201
211
  const createButton = (symbol) => {
202
212
  // Create a response button for this symbol
203
- // TODO why aren't these "button"s??? More accessible, make label easier. Did I have a good reason???
204
213
  let button = document.createElement("button");
205
214
  button.id = symbol;
206
215
  button.className = "response-button";
@@ -235,18 +244,14 @@ class Keypad extends KeypadPeer {
235
244
  button.addEventListener("touchmove", (e) => {
236
245
  /* prevent delay and simulated mouse events */
237
246
  e.preventDefault();
238
- console.log("touchmove event: ", e);
239
247
  });
240
248
  button.addEventListener("touchend", (e) => {
241
249
  /* prevent delay and simulated mouse events */
242
250
  e.preventDefault();
243
- console.log("touchend event: ", e);
244
251
  const elementEndedOn = document.elementFromPoint(
245
252
  e.changedTouches[0].clientX,
246
253
  e.changedTouches[0].clientY
247
254
  );
248
- console.log("elementEndedOn: ", elementEndedOn);
249
- console.log("elementEndedOn.className: ", elementEndedOn.className);
250
255
  switch (elementEndedOn.className) {
251
256
  case "response-button-label noselect":
252
257
  buttonResponseFn(elementEndedOn.parentElement); // e.target.click();
@@ -293,6 +298,8 @@ class Keypad extends KeypadPeer {
293
298
  this.clearKeys();
294
299
  // Create new buttons
295
300
  this.alphabet.forEach((symbol) => createButton(symbol));
301
+ // Manually style buttons, according to Denis' algorithm
302
+ setTimeout(() => applyMaxKeySize(this.alphabet.length), 100);
296
303
  };
297
304
  visualFeedbackThenReset = (delayTime = 800) => {
298
305
  // ie grey out keys just after use, to discourage rapid response
@@ -336,8 +343,8 @@ class Keypad extends KeypadPeer {
336
343
  * Remove all keys from the keypad.
337
344
  */
338
345
  clearKeys = () => {
339
- document.querySelector("#keypad-keys").innerHTML = "";
340
- document.querySelector("#keypad-control-keys").innerHTML = "";
346
+ document.querySelector("#keypad-keys").innerHTML = "<div id='keypad-control-keys'></div>";
347
+ // document.querySelector("#keypad-control-keys").innerHTML = "";
341
348
  };
342
349
  /**
343
350
  * Return the nodes corresponding to the specified keys.
@@ -354,7 +361,6 @@ class Keypad extends KeypadPeer {
354
361
  */
355
362
  disableKeys = (whichKeys=[]) => {
356
363
  const keyElems = this._getKeysElements(whichKeys);
357
- console.log("disabling elems", keyElems);
358
364
  keyElems.forEach(e => {
359
365
  e.classList.add("unpressable");
360
366
  e.classList.add("noselect");
package/src/keypadPeer.js CHANGED
@@ -17,8 +17,6 @@ export class KeypadPeer {
17
17
  targetElementId: null,
18
18
  }
19
19
  ) {
20
- /* Create the Peer object for our end of the connection. */
21
- this.peer = new Peer(null, { debug: 2 });
22
20
  this.conn = null;
23
21
  this.lastPeerId = null;
24
22
  this.keypadUrl = parameters.hasOwnProperty("keypadUrl")
@@ -27,6 +25,16 @@ export class KeypadPeer {
27
25
  this.targetElement = parameters.hasOwnProperty("targetElementId")
28
26
  ? parameters.targetElementId
29
27
  : null;
28
+ // TODO add support for ttd (ms)
29
+ this.ttd = parameters.hasOwnProperty("ttd") ? parameters.hasOwnProperty("ttd") : 8000;
30
+ // TODO add support for heartRate (ms)
31
+ this.heartbeatIntervalMs = parameters.hasOwnProperty("heartRate") ? parameters.hasOwnProperty("heartRate") : 2000;
32
+ this.lastHeartbeat = performance.now();
33
+ this.heartBeatInterval = undefined;
34
+ this.heartCheckInterval = undefined
35
+
36
+ /* Create the Peer object for our end of the connection. */
37
+ this.peer = new Peer(null, { pingInterval: this.heartbeatIntervalMs, debug: 2 });
30
38
 
31
39
  this.alphabet = null;
32
40
  this.font = null;
@@ -86,7 +94,7 @@ export class KeypadPeer {
86
94
  // STRING : ok
87
95
  if (
88
96
  proposedAlphabet.toUpperCase() === "SPACE" ||
89
- proposedAlphabet.toUpperCase() == "ESC"
97
+ proposedAlphabet.toUpperCase() == "RETURN"
90
98
  ) {
91
99
  validAlphabet = [proposedAlphabet];
92
100
  } else {
@@ -95,25 +103,41 @@ export class KeypadPeer {
95
103
  } else {
96
104
  // SOMETHING ELSE : bad
97
105
  console.error(
98
- "Error! Alphabet must be specified as an array of symbols, including 'ESC', 'SPACE'"
106
+ "Error! Alphabet must be specified as an array of symbols, including 'RETURN', 'SPACE'"
99
107
  );
100
108
  validAlphabet = [];
101
109
  }
102
110
  // Return unique elements, see: https://stackoverflow.com/questions/11246758/how-to-get-unique-values-in-an-array
103
111
  const uniqueValidAlphabet = [...new Set(validAlphabet)];
104
112
 
105
- // Order alphabet so that if 'SPACE' and 'ESC' are in the list, they are correctly positioned
113
+ // Order alphabet so that if 'SPACE' and 'RETURN' are in the list, they are correctly positioned
106
114
  if ("SPACE" in uniqueValidAlphabet) {
107
115
  uniqueValidAlphabet = moveElementToEndOfArray(
108
116
  uniqueValidAlphabet,
109
117
  "SPACE"
110
118
  );
111
119
  }
112
- if ("ESC" in uniqueValidAlphabet) {
113
- uniqueValidAlphabet = moveElementToEndOfArray(uniqueValidAlphabet, "ESC");
120
+ if ("RETURN" in uniqueValidAlphabet) {
121
+ uniqueValidAlphabet = moveElementToEndOfArray(uniqueValidAlphabet, "RETURN");
114
122
  }
115
123
  return uniqueValidAlphabet;
116
124
  };
125
+ _setupHeartBeatIntervals = () => {
126
+ this.heartBeatInterval = setInterval(() => this.conn?.send({ message: "Heartbeat", }) , this.heartbeatIntervalMs);
127
+
128
+ this.heartCheckInterval = setInterval(() => {
129
+ const timeSinceHeartbeatMs = performance.now() - this.lastHeartbeat;
130
+ if (timeSinceHeartbeatMs > this.ttd) {
131
+ console.log("Closing connection due to lack of heartbeat.");
132
+ this.conn?.close();
133
+ this.conn = undefined;
134
+ clearInterval(this.heartBeatInterval);
135
+ clearInterval(this.heartCheckInterval);
136
+ this.heartBeatInterval = undefined;
137
+ this.heartCheckInterval = undefined;
138
+ }
139
+ }, this.ttd);
140
+ };
117
141
  }
118
142
 
119
143
  const moveElementToEndOfArray = (array, element) => {
@@ -0,0 +1,82 @@
1
+
2
+ const getKeysDimensions = (elem, n, aspect=0.5) => {
3
+ /**** translation of maxKeySize.m, original provided courtesy of Prof Denis Pelli ****/
4
+ let keyHeightPx;
5
+
6
+ // key = aspect*keyHeightPx × keyHeightPx
7
+ const widthPx = elem.clientWidth;
8
+ const heightPx = elem.clientHeight;
9
+ let screenArea=widthPx*heightPx
10
+
11
+ keyHeightPx= (-1+Math.sqrt(1+4*n*heightPx/widthPx))/(2*n/widthPx);
12
+ keyHeightPx = (-1+Math.sqrt(1+4*n*heightPx*aspect/widthPx))/(2*aspect*n/widthPx);
13
+
14
+ console.log(`n=${n}, aspect=${aspect}, widthPx=${widthPx}, heightPx=${heightPx}`);
15
+ console.log(`100% efficiency would require keyHightPx ${keyHeightPx.toFixed(1)}`);
16
+ while (n > (Math.floor(heightPx/keyHeightPx - 1)*Math.floor(widthPx/(aspect*keyHeightPx)))){
17
+ // Round size down so that screen is a multiple.
18
+ const ss=[heightPx/Math.ceil(heightPx/keyHeightPx), widthPx/Math.ceil(widthPx/(aspect*keyHeightPx))];
19
+ for (let i=0; i<=ss.length; i++) {
20
+ // We’ve already rejected the current size, so only consider smaller sizes.
21
+ // This guarantees that sizePx will decrease on every loop iteration.
22
+ if (ss[i]>=keyHeightPx) ss[i]=0;
23
+ }
24
+ // We want the largest possible size so try the largest of the current options.
25
+ keyHeightPx=Math.max(...ss);
26
+ }
27
+
28
+ // Compute efficiency as area covered by keys divided by screen area.
29
+ // First term for n keys. Second term is for Space and Return, which fully
30
+ // occupy their row.
31
+ const areaOfKeys=aspect*keyHeightPx*keyHeightPx*n + widthPx*keyHeightPx;
32
+ screenArea=heightPx*widthPx;
33
+ const numKeysHorizontally = Math.floor(widthPx/(aspect*keyHeightPx));
34
+ const numKeysVertically = Math.floor(heightPx/keyHeightPx-1);
35
+ const efficiency = 100*areaOfKeys/screenArea;
36
+ console.log(`Best keyheightPx ${keyHeightPx} px. ${numKeysHorizontally} horizontally x ${numKeysVertically} vertically. Efficiency ${efficiency}`)
37
+
38
+ return {keyHeightPx: keyHeightPx, cols: numKeysHorizontally, rows:numKeysVertically, widthPx: widthPx, heightPx: heightPx}
39
+ };
40
+
41
+ export const applyMaxKeySize = (numberOfKeys) => {
42
+ const aspect = 1;
43
+ const keysElem = document.getElementById("keypad");
44
+ const {keyHeightPx, cols, rows, widthPx, heightPx} = getKeysDimensions(keysElem, numberOfKeys, aspect);
45
+ const keyElems = [...keysElem.getElementsByClassName("response-button")];
46
+ const controlKeyElemsMask = keyElems.map(e => e.parentNode.id === "keypad-control-keys");
47
+ const gridCoords = keyElems.filter((k,i) => !controlKeyElemsMask[i]).map((k,i) => [Math.floor(i/cols), (cols-1)-(i%cols)]);
48
+ const widthUsed = cols*(keyHeightPx*aspect);
49
+ const heightUsed = rows*keyHeightPx + keyHeightPx;
50
+ console.log("gridCords", gridCoords);
51
+ const freeHeight = heightPx - heightUsed;
52
+ const freeWidth = widthPx - widthUsed;
53
+ const verticalMarginOffset = Math.floor(freeHeight/2);
54
+ const horizontalMarginOffset = Math.floor(freeWidth/2);
55
+
56
+ let j=0;
57
+ keyElems.forEach((k,i) => {
58
+ k.style.position = "fixed";
59
+ const controlKey = controlKeyElemsMask[i];
60
+ const height = `${keyHeightPx}px`;
61
+ let top, left, width;
62
+ if (controlKey) {
63
+ top = (heightPx-keyHeightPx) - verticalMarginOffset + "px";
64
+ left = (k.innerText.toLowerCase() === "space" ? 0 : widthPx/2 - horizontalMarginOffset) + horizontalMarginOffset + "px";
65
+ width = `${(widthPx - horizontalMarginOffset*2)/2}px`;
66
+ } else {
67
+ width = keyHeightPx*aspect;
68
+ const [y,x] = gridCoords[j];
69
+ j += 1;
70
+ console.log(`[x,y]: [${x},${y}]`);
71
+ top = y*keyHeightPx + verticalMarginOffset + "px";
72
+ left = `${x*width+horizontalMarginOffset}px`;
73
+ width += "px";
74
+ }
75
+ console.log(`${k.id}, h: ${height}, w: ${width}, top: ${top}, left: ${left}`);
76
+ k.style.width = width;
77
+ k.style.height = height;
78
+ k.style.top = top;
79
+ k.style.left = left;
80
+ });
81
+ console.log("\n");
82
+ };
package/src/receiver.js CHANGED
@@ -22,7 +22,7 @@ class Receiver extends KeypadPeer {
22
22
  this.font = keypadParameters["font"]; // What fontface to display the symbols in
23
23
 
24
24
  this.onData = onDataCallback; // What to do on a button-press
25
- this.onHandshake = handshakeCallback; // What to do when the connection is established
25
+ this.onHandshake = () => {handshakeCallback(); this._setupHeartBeatIntervals();} // What to do when the connection is established
26
26
  this.onConnection = (connection) => {this.#onPeerConnection(connection); customConnectionCallback(connection)};
27
27
  this.onClose = () => {this.onPeerClose(); customCloseCallback()};
28
28
  this.onError = (err) => {this.onPeerError(err); customErrorCallback(err)};
@@ -198,6 +198,10 @@ class Receiver extends KeypadPeer {
198
198
  case "Keypress":
199
199
  this.onData(data);
200
200
  break;
201
+ // TODO factor out into keypadPeer
202
+ case "Heartbeat":
203
+ this.lastHeartbeat = performance.now();
204
+ break;
201
205
  default:
202
206
  console.log("Message type: ", data.message);
203
207
  }