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/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
- const getKeysDimensions = (elem, n, aspect = 0.5) => {
2
- /**** translation of maxKeySize.m, original provided courtesy of Prof Denis Pelli ****/
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
- // Weve already rejected the current size, so only consider smaller sizes.
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 the largest possible size so try the largest of the current options.
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
- export const applyMaxKeySize = (numberOfKeys) => {
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
- let margin = 5;
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.parentNode.id === "keypad-control-keys",
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, width;
279
+ let top, left;
280
+
89
281
  if (controlKey) {
90
- top = heightPx - keyHeightPx;
91
- width = (widthPx / numControlKeys) - margin;
282
+ const widthNum = (widthPx / numControlKeys) - margin;
283
+ top = `${heightPx - keyHeightPx}px`;
92
284
  const m = margin * 0.5;
93
- const controlKeyIndex = controlKeys.length;
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 + "px";
289
+ k.style.height = `${keyHeightPx - margin}px`;
290
+ k.style.width = `${widthNum}px`;
101
291
  } else {
102
292
  const height = keyHeightPx - margin;
103
- width = height * aspect;
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 * width + (x + 1) * margin + horizontalMarginOffset - margin / 2;
108
- if (!keyFontSize) {
109
- keyFontSize = getLargeFontSize(k, width, height);
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 + "px";
112
- k.style.fontSize = height / 2 + "px";
302
+ k.style.height = `${height}px`;
303
+ k.style.fontSize = `${height / 2}px`;
304
+ k.style.width = `${widthNum}px`;
113
305
  }
114
- k.style.width = width + "px";
115
- k.style.top = top + "px";
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
- const getLargeFontSize = (k, width, height) => {
124
- k.style.height = "auto";
125
- k.style.width = "auto";
126
- k.style.whiteSpace = "nowrap";
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('path');
1
+ const path = require("path");
2
2
 
3
3
  module.exports = {
4
- entry: './src/main.js',
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, 'dist'),
7
- filename: '[name].js',
8
- library: 'virtualKeypad',
9
- libraryTarget: 'umd',
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: 'file-loader',
22
+ loader: "file-loader",
21
23
  options: {
22
- name: '[name].[ext]',
23
- outputPath: 'fonts/'
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
  };