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/main.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// import './keypadPeer.js';
|
|
2
2
|
import { Keypad } from './keypad.js';
|
|
3
3
|
import { Receiver } from './receiver.js';
|
|
4
|
+
import { getMaxPossibleFontSize, getOptimalSharedFontSize } from './maxKeySize.js';
|
|
4
5
|
|
|
5
|
-
export { Receiver, Keypad };
|
|
6
|
+
export { Receiver, Keypad, getMaxPossibleFontSize, getOptimalSharedFontSize };
|
package/src/maxKeySize.js
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
/**** translation of maxKeySize.m, original provided courtesy of Prof Denis Pelli ****/
|
|
2
|
+
/**
|
|
3
|
+
* Find dimensions for keypad keys that maximize space usage.
|
|
4
|
+
*
|
|
5
|
+
* @param {HTMLElement} elem - The keypad container element
|
|
6
|
+
* @param {number} n - Number of alphabet keys
|
|
7
|
+
* @param {number} aspect - Key aspect ratio (default: 0.5)
|
|
8
|
+
* @returns {Object} Key dimensions including sizes and layout
|
|
9
|
+
*/
|
|
10
|
+
export const getKeysDimensions = (elem, n, aspect = 0.5) => {
|
|
3
11
|
let keyHeightPx;
|
|
4
12
|
|
|
5
13
|
// key = aspect*keyHeightPx × keyHeightPx
|
|
@@ -24,11 +32,11 @@ const getKeysDimensions = (elem, n, aspect = 0.5) => {
|
|
|
24
32
|
widthPx / Math.ceil(widthPx / (aspect * keyHeightPx)),
|
|
25
33
|
];
|
|
26
34
|
for (let i = 0; i <= ss.length; i++) {
|
|
27
|
-
// We
|
|
35
|
+
// We've already rejected current size, so only consider smaller sizes.
|
|
28
36
|
// This guarantees that sizePx will decrease on every loop iteration.
|
|
29
37
|
if (ss[i] >= keyHeightPx) ss[i] = 0;
|
|
30
38
|
}
|
|
31
|
-
// We want
|
|
39
|
+
// We want largest possible size so try to use largest of current options.
|
|
32
40
|
keyHeightPx = Math.max(...ss);
|
|
33
41
|
}
|
|
34
42
|
|
|
@@ -51,19 +59,203 @@ const getKeysDimensions = (elem, n, aspect = 0.5) => {
|
|
|
51
59
|
};
|
|
52
60
|
};
|
|
53
61
|
|
|
54
|
-
|
|
62
|
+
/**
|
|
63
|
+
* Calculate font size for a button element.
|
|
64
|
+
*
|
|
65
|
+
* @param {HTMLElement} k - Button element
|
|
66
|
+
* @param {number} width - Target width in pixels
|
|
67
|
+
* @param {number} height - Target height in pixels
|
|
68
|
+
* @param {boolean} enableWrap - Whether to enable text wrapping (default: true)
|
|
69
|
+
* @returns {number} Font size in pixels
|
|
70
|
+
*/
|
|
71
|
+
const getLargeFontSize = (k, width, height, enableWrap = true) => {
|
|
72
|
+
k.style.height = "auto";
|
|
73
|
+
k.style.width = "auto";
|
|
74
|
+
|
|
75
|
+
if (enableWrap) {
|
|
76
|
+
// Enable line and word breaking
|
|
77
|
+
k.style.whiteSpace = "pre-wrap";
|
|
78
|
+
k.style.wordWrap = "break-word";
|
|
79
|
+
k.style.wordBreak = "normal";
|
|
80
|
+
k.style.overflow = "hidden";
|
|
81
|
+
} else {
|
|
82
|
+
k.style.whiteSpace = "nowrap";
|
|
83
|
+
k.style.wordWrap = "normal";
|
|
84
|
+
k.style.wordBreak = "normal";
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Binary search for largest font size
|
|
88
|
+
let low = 1;
|
|
89
|
+
let high = 200; // Maximum reasonable font size
|
|
90
|
+
let bestFit = low;
|
|
91
|
+
|
|
92
|
+
while (low <= high) {
|
|
93
|
+
const mid = Math.round(Math.exp((Math.log(low) + Math.log(high)) / 2));
|
|
94
|
+
k.style.fontSize = `${mid}px`;
|
|
95
|
+
|
|
96
|
+
// Measure actual rendered size
|
|
97
|
+
const rect = k.getBoundingClientRect();
|
|
98
|
+
const fitsWidth = rect.width <= width;
|
|
99
|
+
const fitsHeight = rect.height <= height;
|
|
100
|
+
const isValid = rect.width > 0 && rect.height > 0;
|
|
101
|
+
|
|
102
|
+
if (fitsWidth && fitsHeight && isValid) {
|
|
103
|
+
bestFit = mid;
|
|
104
|
+
low = mid + 1;
|
|
105
|
+
} else {
|
|
106
|
+
high = mid - 1;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return bestFit;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Binary-search for the largest font size (px) at which `text` fits within
|
|
115
|
+
* the given pixel dimensions, allowing word-wrap.
|
|
116
|
+
*
|
|
117
|
+
* @param {string} text - The text to measure
|
|
118
|
+
* @param {number} widthPx - Available width in pixels
|
|
119
|
+
* @param {number} heightPx - Available height in pixels
|
|
120
|
+
* @param {string} fontFamily - CSS font-family string
|
|
121
|
+
* @param {number} lineHeight - CSS line-height (unitless multiplier, default 1.2)
|
|
122
|
+
* @returns {number} Largest font size in whole pixels that fits
|
|
123
|
+
*/
|
|
124
|
+
export const getMaxPossibleFontSize = (text, widthPx, heightPx, fontFamily, lineHeight = 1.2, fontWeight = 'normal') => {
|
|
125
|
+
const testDiv = document.createElement('div');
|
|
126
|
+
testDiv.style.position = 'absolute';
|
|
127
|
+
testDiv.style.visibility = 'hidden';
|
|
128
|
+
testDiv.style.width = `${widthPx}px`;
|
|
129
|
+
testDiv.style.fontFamily = fontFamily;
|
|
130
|
+
testDiv.style.lineHeight = String(lineHeight);
|
|
131
|
+
testDiv.style.fontWeight = fontWeight;
|
|
132
|
+
testDiv.style.whiteSpace = 'pre-wrap';
|
|
133
|
+
testDiv.style.wordWrap = 'break-word';
|
|
134
|
+
testDiv.style.wordBreak = 'normal';
|
|
135
|
+
testDiv.style.overflow = 'hidden';
|
|
136
|
+
testDiv.style.padding = '0';
|
|
137
|
+
testDiv.style.margin = '0';
|
|
138
|
+
testDiv.textContent = text;
|
|
139
|
+
document.body.appendChild(testDiv);
|
|
140
|
+
|
|
141
|
+
let low = 1;
|
|
142
|
+
let high = 200;
|
|
143
|
+
let bestFit = low;
|
|
144
|
+
|
|
145
|
+
while (low <= high) {
|
|
146
|
+
const mid = Math.floor((low + high) / 2);
|
|
147
|
+
testDiv.style.fontSize = `${mid}px`;
|
|
148
|
+
|
|
149
|
+
if (testDiv.scrollHeight <= heightPx && testDiv.scrollWidth <= Math.ceil(widthPx)) {
|
|
150
|
+
bestFit = mid;
|
|
151
|
+
low = mid + 1;
|
|
152
|
+
} else {
|
|
153
|
+
high = mid - 1;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
document.body.removeChild(testDiv);
|
|
158
|
+
return bestFit;
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Find the largest shared font size for a set of buttons by trying n=1..4
|
|
163
|
+
* virtual line-counts (effective width = clientWidth/n). Picks the n where
|
|
164
|
+
* the minimum font size across all buttons is maximised, then applies that
|
|
165
|
+
* font size and—when n>1—constrains the .response-button-label span width
|
|
166
|
+
* so the browser's own text engine (including CJK) performs the line breaks.
|
|
167
|
+
*
|
|
168
|
+
* @param {HTMLElement[]} buttons - Buttons that must share one font size
|
|
169
|
+
* @param {number} lineHeight - CSS line-height multiplier (default 1.15)
|
|
170
|
+
*/
|
|
171
|
+
export const getOptimalSharedFontSize = (buttons, lineHeight = 1.15) => {
|
|
172
|
+
if (buttons.length === 0) return;
|
|
173
|
+
|
|
174
|
+
let bestFontSize = 0;
|
|
175
|
+
let bestN = 1;
|
|
176
|
+
|
|
177
|
+
for (let n = 1; n <= 4; n++) {
|
|
178
|
+
const sizes = buttons.map(b => {
|
|
179
|
+
const label = b.querySelector('.response-button-label');
|
|
180
|
+
const text = (label?.textContent ?? b.textContent) ?? '';
|
|
181
|
+
const style = getComputedStyle(b);
|
|
182
|
+
const labelStyle = label ? getComputedStyle(label) : null;
|
|
183
|
+
const hPad = parseFloat(style.paddingLeft) + parseFloat(style.paddingRight)
|
|
184
|
+
+ (labelStyle ? parseFloat(labelStyle.paddingLeft) + parseFloat(labelStyle.paddingRight) : 0);
|
|
185
|
+
const vPad = parseFloat(style.paddingTop) + parseFloat(style.paddingBottom)
|
|
186
|
+
+ (labelStyle ? parseFloat(labelStyle.paddingTop) + parseFloat(labelStyle.paddingBottom) : 0);
|
|
187
|
+
return getMaxPossibleFontSize(
|
|
188
|
+
text,
|
|
189
|
+
(b.clientWidth - hPad) / n,
|
|
190
|
+
b.clientHeight - vPad, // buttons have explicit height set by applyMaxKeySize
|
|
191
|
+
style.fontFamily,
|
|
192
|
+
lineHeight,
|
|
193
|
+
style.fontWeight,
|
|
194
|
+
);
|
|
195
|
+
});
|
|
196
|
+
const minSize = Math.min(...sizes);
|
|
197
|
+
if (minSize > bestFontSize) {
|
|
198
|
+
bestFontSize = minSize;
|
|
199
|
+
bestN = n;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Capture widths before applying font (larger font would otherwise expand buttons)
|
|
204
|
+
const preWidths = buttons.map(b => b.clientWidth);
|
|
205
|
+
|
|
206
|
+
const applyFontSize = (size) => {
|
|
207
|
+
buttons.forEach((b, i) => {
|
|
208
|
+
b.style.width = `${preWidths[i]}px`;
|
|
209
|
+
b.style.fontSize = `${size}px`;
|
|
210
|
+
b.style.lineHeight = String(lineHeight);
|
|
211
|
+
const label = b.querySelector('.response-button-label');
|
|
212
|
+
if (label) {
|
|
213
|
+
label.style.fontSize = `${size}px`;
|
|
214
|
+
if (bestN > 1) {
|
|
215
|
+
label.style.maxWidth = `${Math.floor(preWidths[i] / bestN)}px`;
|
|
216
|
+
label.style.setProperty('text-wrap', 'balance');
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
applyFontSize(bestFontSize);
|
|
223
|
+
|
|
224
|
+
// Post-hoc: verify no label overflows in the actual DOM (handles sub-pixel/browser differences)
|
|
225
|
+
while (bestFontSize > 1) {
|
|
226
|
+
const overflows = buttons.some(b => {
|
|
227
|
+
const label = b.querySelector('.response-button-label');
|
|
228
|
+
return label && (label.scrollWidth > label.clientWidth || label.scrollHeight > label.clientHeight);
|
|
229
|
+
});
|
|
230
|
+
if (!overflows) break;
|
|
231
|
+
bestFontSize -= 1;
|
|
232
|
+
applyFontSize(bestFontSize);
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Apply maximum key size to keypad.
|
|
238
|
+
*
|
|
239
|
+
* @param {number} numberOfKeys - Number of alphabet keys to display
|
|
240
|
+
* @param {string} fontFamily - Font family to use (passed from keypad)
|
|
241
|
+
*/
|
|
242
|
+
export const applyMaxKeySize = (numberOfKeys, fontFamily = 'sans-serif') => {
|
|
55
243
|
const aspect = 1;
|
|
56
|
-
|
|
244
|
+
const margin = 5;
|
|
57
245
|
const keysElem = document.getElementById("keypad");
|
|
246
|
+
if (!keysElem) return;
|
|
247
|
+
|
|
58
248
|
const { keyHeightPx, cols, rows, widthPx, heightPx } = getKeysDimensions(
|
|
59
249
|
keysElem,
|
|
60
250
|
numberOfKeys,
|
|
61
251
|
aspect,
|
|
62
252
|
);
|
|
63
253
|
|
|
254
|
+
if (widthPx <= 0 || heightPx <= 0 || keyHeightPx <= margin) return;
|
|
255
|
+
|
|
64
256
|
const keyElems = [...keysElem.getElementsByClassName("response-button")];
|
|
65
257
|
const controlKeyElemsMask = keyElems.map(
|
|
66
|
-
(e) => e.
|
|
258
|
+
(e) => e.parentElement?.id === "keypad-control-keys",
|
|
67
259
|
);
|
|
68
260
|
const gridCoords = keyElems
|
|
69
261
|
.filter((k, i) => !controlKeyElemsMask[i])
|
|
@@ -76,65 +268,48 @@ export const applyMaxKeySize = (numberOfKeys) => {
|
|
|
76
268
|
const horizontalMarginOffset = Math.floor(freeWidth / 2);
|
|
77
269
|
|
|
78
270
|
const controlKeys = [];
|
|
79
|
-
let controlKeyFontSize = Infinity;
|
|
80
271
|
let keyFontSize;
|
|
81
272
|
let j = 0;
|
|
82
|
-
|
|
83
|
-
const numControlKeys = controlKeyElemsMask.filter(x => x).length;
|
|
273
|
+
|
|
274
|
+
const numControlKeys = controlKeyElemsMask.filter((x) => x).length;
|
|
84
275
|
|
|
85
276
|
keyElems.forEach((k, i) => {
|
|
86
277
|
k.style.position = "fixed";
|
|
87
278
|
const controlKey = controlKeyElemsMask[i];
|
|
88
|
-
let top, left
|
|
279
|
+
let top, left;
|
|
280
|
+
|
|
89
281
|
if (controlKey) {
|
|
90
|
-
|
|
91
|
-
|
|
282
|
+
const widthNum = (widthPx / numControlKeys) - margin;
|
|
283
|
+
top = `${heightPx - keyHeightPx}px`;
|
|
92
284
|
const m = margin * 0.5;
|
|
93
|
-
|
|
94
|
-
left = m + controlKeyIndex * (width + margin);
|
|
285
|
+
left = `${m + controlKeys.length * (widthNum + margin)}px`;
|
|
95
286
|
|
|
96
|
-
const f = getLargeFontSize(k, width, keyHeightPx - margin);
|
|
97
|
-
controlKeyFontSize = Math.min(f, controlKeyFontSize);
|
|
98
287
|
controlKeys.push(k);
|
|
99
288
|
k.style.borderRadius = "25px";
|
|
100
|
-
k.style.height = keyHeightPx - margin
|
|
289
|
+
k.style.height = `${keyHeightPx - margin}px`;
|
|
290
|
+
k.style.width = `${widthNum}px`;
|
|
101
291
|
} else {
|
|
102
292
|
const height = keyHeightPx - margin;
|
|
103
|
-
|
|
293
|
+
const widthNum = height * aspect;
|
|
104
294
|
const [y, x] = gridCoords[j];
|
|
105
295
|
j += 1;
|
|
106
|
-
top = y * height + (y + 1) * margin + verticalMarginOffset - margin / 2
|
|
107
|
-
left = x *
|
|
108
|
-
|
|
109
|
-
|
|
296
|
+
top = `${y * height + (y + 1) * margin + verticalMarginOffset - margin / 2}px`;
|
|
297
|
+
left = `${x * widthNum + (x + 1) * margin + horizontalMarginOffset - margin / 2}px`;
|
|
298
|
+
|
|
299
|
+
if (keyFontSize === undefined) {
|
|
300
|
+
keyFontSize = getLargeFontSize(k, widthNum, height);
|
|
110
301
|
}
|
|
111
|
-
k.style.height = height
|
|
112
|
-
k.style.fontSize = height / 2
|
|
302
|
+
k.style.height = `${height}px`;
|
|
303
|
+
k.style.fontSize = `${height / 2}px`;
|
|
304
|
+
k.style.width = `${widthNum}px`;
|
|
113
305
|
}
|
|
114
|
-
k.style.
|
|
115
|
-
k.style.
|
|
116
|
-
k.style.left = left + "px";
|
|
306
|
+
k.style.top = top;
|
|
307
|
+
k.style.left = left;
|
|
117
308
|
k.style.visibility = "visible";
|
|
118
309
|
});
|
|
119
|
-
// Set the control keys to the same size, the smaller of the two
|
|
120
|
-
controlKeys.forEach((k) => (k.style.fontSize = controlKeyFontSize + "px"));
|
|
121
|
-
};
|
|
122
310
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
// Set to a nominal font size, s
|
|
129
|
-
const s = 20;
|
|
130
|
-
k.style.fontSize = s + "px";
|
|
131
|
-
// Measure width of elem, w
|
|
132
|
-
const w = k.getBoundingClientRect().width;
|
|
133
|
-
const h = k.getBoundingClientRect().height;
|
|
134
|
-
const rW = 1 / (w / width);
|
|
135
|
-
const rH = 1 / (h / height);
|
|
136
|
-
const r = Math.min(rW, rH);
|
|
137
|
-
// Set font size to s*r
|
|
138
|
-
const f = Math.floor(s * r);
|
|
139
|
-
return f;
|
|
311
|
+
// Calculate and apply optimal shared font size for control buttons
|
|
312
|
+
if (controlKeys.length > 0) {
|
|
313
|
+
getOptimalSharedFontSize(controlKeys);
|
|
314
|
+
}
|
|
140
315
|
};
|
package/webpack.common.js
CHANGED
|
@@ -1,30 +1,58 @@
|
|
|
1
|
-
const path = require(
|
|
1
|
+
const path = require("path");
|
|
2
2
|
|
|
3
3
|
module.exports = {
|
|
4
|
-
entry:
|
|
4
|
+
entry: {
|
|
5
|
+
main: "./src/main.js",
|
|
6
|
+
PhonePeer: "./src/PhonePeer.js",
|
|
7
|
+
ExperimentPeer: "./src/ExperimentPeer.js",
|
|
8
|
+
},
|
|
5
9
|
output: {
|
|
6
|
-
path: path.resolve(__dirname,
|
|
7
|
-
filename:
|
|
8
|
-
library:
|
|
9
|
-
libraryTarget:
|
|
10
|
+
path: path.resolve(__dirname, "dist"),
|
|
11
|
+
filename: "[name].js",
|
|
12
|
+
library: "virtualKeypad",
|
|
13
|
+
libraryTarget: "umd",
|
|
10
14
|
},
|
|
11
15
|
module: {
|
|
12
16
|
rules: [
|
|
13
|
-
{ test: /\.css$/,
|
|
14
|
-
use: [ 'style-loader', 'css-loader', ],
|
|
15
|
-
},
|
|
17
|
+
{ test: /\.css$/, use: ["style-loader", "css-loader"] },
|
|
16
18
|
{
|
|
17
19
|
test: /\.(woff(2)?|ttf|eot|svg)(\?v=\d+\.\d+\.\d+)?$/,
|
|
18
20
|
use: [
|
|
19
21
|
{
|
|
20
|
-
loader:
|
|
22
|
+
loader: "file-loader",
|
|
21
23
|
options: {
|
|
22
|
-
name:
|
|
23
|
-
outputPath:
|
|
24
|
-
}
|
|
25
|
-
}
|
|
24
|
+
name: "[name].[ext]",
|
|
25
|
+
outputPath: "fonts/",
|
|
26
|
+
},
|
|
27
|
+
},
|
|
26
28
|
],
|
|
27
29
|
},
|
|
28
|
-
|
|
29
|
-
|
|
30
|
+
{
|
|
31
|
+
// If you have .mjs files, use test: /\.m?js$/,
|
|
32
|
+
test: /\.js$/,
|
|
33
|
+
exclude: /node_modules/,
|
|
34
|
+
use: {
|
|
35
|
+
loader: "babel-loader",
|
|
36
|
+
options: {
|
|
37
|
+
// You can also move this config into a separate .babelrc or babel.config.js file
|
|
38
|
+
presets: [
|
|
39
|
+
[
|
|
40
|
+
"@babel/preset-env",
|
|
41
|
+
{
|
|
42
|
+
// Adjust your target browsers as needed
|
|
43
|
+
targets: {
|
|
44
|
+
ios: "12",
|
|
45
|
+
},
|
|
46
|
+
// This config tells Babel to automatically include necessary polyfills
|
|
47
|
+
// for features you use, referencing core-js where needed
|
|
48
|
+
useBuiltIns: "usage",
|
|
49
|
+
corejs: "3",
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
],
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
},
|
|
30
58
|
};
|