js_lis 2.0.8 → 3.0.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.
@@ -14,10 +14,17 @@ export class VirtualKeyboard {
14
14
  this.shiftActive = false;
15
15
  this.capsLockActive = false;
16
16
  this.isScrambled = false;
17
- this.isVirtualKeyboardActive = false;
17
+ this.isVirtualKeyboardActive = false;
18
18
 
19
19
  this.inputPlaintextBuffers = new WeakMap();
20
-
20
+
21
+ // FIX BUG-2: เก็บ bound reference เพื่อให้ removeEventListener ทำงานได้
22
+ this._boundDrag = this.drag.bind(this);
23
+ this._boundStopDrag = this.stopDrag.bind(this);
24
+
25
+ // FIX WARN-3: เก็บ flag ว่า listeners ถูก add ไปแล้วหรือยัง
26
+ this._listenersInitialized = false;
27
+
21
28
  this.initialize();
22
29
  }
23
30
 
@@ -25,7 +32,11 @@ export class VirtualKeyboard {
25
32
  try {
26
33
  injectCSS();
27
34
  this.render();
28
- this.initializeInputListeners();
35
+ // FIX WARN-3: เรียก initializeInputListeners ครั้งเดียวเท่านั้น
36
+ if (!this._listenersInitialized) {
37
+ this.initializeInputListeners();
38
+ this._listenersInitialized = true;
39
+ }
29
40
  console.log("VirtualKeyboard initialized successfully.");
30
41
  } catch (error) {
31
42
  console.error("Error initializing VirtualKeyboard:", error);
@@ -65,41 +76,118 @@ export class VirtualKeyboard {
65
76
  }
66
77
  });
67
78
 
68
- document.addEventListener("focus", (e) => {
79
+ document.addEventListener(
80
+ "focus",
81
+ (e) => {
69
82
  const target = e.target;
70
83
  if (target.tagName === "INPUT" || target.tagName === "TEXTAREA") {
71
84
  this.setCurrentInput(target, !this.isVirtualKeyboardActive);
72
85
  }
73
86
  },
74
- true
87
+ true,
75
88
  );
76
89
 
77
- //Add real keyboard input listeners to handle encryption
90
+ // FIX BUG-4: handle paste event เพื่อ sync buffer
91
+ document.addEventListener("paste", (e) => {
92
+ const target = e.target;
93
+ if (
94
+ (target.tagName === "INPUT" || target.tagName === "TEXTAREA") &&
95
+ target === this.currentInput &&
96
+ !this.isVirtualKeyboardActive
97
+ ) {
98
+ e.preventDefault();
99
+ const pasteText = (e.clipboardData || window.clipboardData).getData(
100
+ "text",
101
+ );
102
+ if (!pasteText) return;
103
+
104
+ const start = target.selectionStart;
105
+ const end = target.selectionEnd;
106
+ const buffer = this.getCurrentBuffer();
107
+ const newBuffer =
108
+ buffer.slice(0, start) + pasteText + buffer.slice(end);
109
+ this.setCurrentBuffer(newBuffer);
110
+ this.updateDisplayedValue(false);
111
+
112
+ const newCursorPos = start + pasteText.length;
113
+ setTimeout(() => {
114
+ target.setSelectionRange(newCursorPos, newCursorPos);
115
+ target.focus();
116
+ }, 0);
117
+
118
+ const event = new Event("input", { bubbles: true });
119
+ target.dispatchEvent(event);
120
+ }
121
+ });
122
+
123
+ // FIX BUG-4: handle cut event เพื่อ sync buffer
124
+ document.addEventListener("cut", (e) => {
125
+ const target = e.target;
126
+ if (
127
+ (target.tagName === "INPUT" || target.tagName === "TEXTAREA") &&
128
+ target === this.currentInput &&
129
+ !this.isVirtualKeyboardActive
130
+ ) {
131
+ const start = target.selectionStart;
132
+ const end = target.selectionEnd;
133
+ if (start === end) return;
134
+
135
+ const buffer = this.getCurrentBuffer();
136
+ const selectedText = buffer.slice(start, end);
137
+
138
+ e.preventDefault();
139
+ e.clipboardData.setData("text/plain", selectedText);
140
+
141
+ const newBuffer = buffer.slice(0, start) + buffer.slice(end);
142
+ this.setCurrentBuffer(newBuffer);
143
+ this.updateDisplayedValue(false);
144
+
145
+ setTimeout(() => {
146
+ target.setSelectionRange(start, start);
147
+ target.focus();
148
+ }, 0);
149
+
150
+ const event = new Event("input", { bubbles: true });
151
+ target.dispatchEvent(event);
152
+ }
153
+ });
154
+
78
155
  document.addEventListener("input", (e) => {
79
156
  const target = e.target;
80
- if ((target.tagName === "INPUT" || target.tagName === "TEXTAREA") && target === this.currentInput) {
157
+ if (
158
+ (target.tagName === "INPUT" || target.tagName === "TEXTAREA") &&
159
+ target === this.currentInput
160
+ ) {
81
161
  if (!this.isVirtualKeyboardActive) {
82
162
  this.handleRealKeyboardInput(target, e);
83
163
  }
84
164
  }
85
165
  });
86
166
 
87
- // Add real keyboard keydown listener for proper character tracking
88
167
  document.addEventListener("keydown", (e) => {
89
168
  const target = e.target;
90
- if ((target.tagName === "INPUT" || target.tagName === "TEXTAREA") && target === this.currentInput) {
91
- // Reset the virtual keyboard flag when real keyboard is used
92
- if (!['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'End'].includes(e.code)) {
169
+ if (
170
+ (target.tagName === "INPUT" || target.tagName === "TEXTAREA") &&
171
+ target === this.currentInput
172
+ ) {
173
+ const navKeys = [
174
+ "ArrowLeft",
175
+ "ArrowRight",
176
+ "ArrowUp",
177
+ "ArrowDown",
178
+ "Home",
179
+ "End",
180
+ ];
181
+
182
+ if (!navKeys.includes(e.code)) {
93
183
  this.isVirtualKeyboardActive = false;
94
184
  }
95
-
96
- // Handle real keyboard input
185
+
97
186
  if (!this.isVirtualKeyboardActive) {
98
187
  this.handleRealKeyboardKeydown(target, e);
99
188
  }
100
-
101
- // Handle arrow keys for all input types
102
- if (['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'End'].includes(e.code)) {
189
+
190
+ if (navKeys.includes(e.code)) {
103
191
  setTimeout(() => {
104
192
  target.focus();
105
193
  }, 0);
@@ -120,29 +208,14 @@ export class VirtualKeyboard {
120
208
 
121
209
  this.currentInput = inputElement;
122
210
  this.currentInput.classList.add("keyboard-active");
123
-
124
211
  this.currentInput.focus();
125
-
126
- // Ensure buffer initialized for this input
212
+
127
213
  if (!this.inputPlaintextBuffers.has(this.currentInput)) {
128
- const ds = this.currentInput.dataset ? this.currentInput.dataset.encrypted : undefined;
129
- let initial = "";
130
- try {
131
- if (ds && window.VKCrypto && typeof window.VKCrypto.decrypt === "function") {
132
- initial = window.VKCrypto.decrypt(ds) || "";
133
- } else {
134
- initial = this.currentInput.value || "";
135
- }
136
- } catch (_) {
137
- initial = this.currentInput.value || "";
138
- }
214
+ const initial = this.currentInput.value || "";
139
215
  this.inputPlaintextBuffers.set(this.currentInput, initial);
140
- this.updateDomEncrypted(this.currentInput, initial);
141
216
  }
142
- // Sync visible with buffer (masking for password)
143
217
  this.updateDisplayedValue();
144
-
145
- // Only move cursor to end on initial focus, not during virtual keyboard operations
218
+
146
219
  if (moveToEnd) {
147
220
  setTimeout(() => {
148
221
  const length = this.currentInput.value.length;
@@ -151,7 +224,6 @@ export class VirtualKeyboard {
151
224
  }
152
225
  }
153
226
 
154
- // [1]
155
227
  render() {
156
228
  const keyboard = document.createElement("div");
157
229
  keyboard.className = `virtual-keyboard ${this.currentLayout}`;
@@ -167,11 +239,11 @@ export class VirtualKeyboard {
167
239
 
168
240
  const layoutSelector = document.createElement("select");
169
241
  layoutSelector.id = "layout-selector";
170
- layoutSelector.setAttribute("aria-label", "Select keyboard layout"); // ✅ เพิ่มบรรทัดนี้
242
+ layoutSelector.setAttribute("aria-label", "Select keyboard layout");
171
243
  layoutSelector.onchange = (e) => this.changeLayout(e.target.value);
172
244
 
173
- const layouts = ["full", "en", "th", "numpad", "symbols"];
174
- layouts.forEach((layout) => {
245
+ const availableLayouts = ["full", "en", "th", "numpad", "symbols"];
246
+ availableLayouts.forEach((layout) => {
175
247
  const option = document.createElement("option");
176
248
  option.value = layout;
177
249
  option.innerText = this.getLayoutName(layout);
@@ -180,7 +252,6 @@ export class VirtualKeyboard {
180
252
  layoutSelector.value = this.currentLayout;
181
253
  controlsContainer.appendChild(layoutSelector);
182
254
 
183
-
184
255
  keyboard.appendChild(controlsContainer);
185
256
 
186
257
  const layout = this.layouts[this.currentLayout];
@@ -194,21 +265,38 @@ export class VirtualKeyboard {
194
265
  keyElement.className = "keyboard-key key";
195
266
  keyElement.textContent = key;
196
267
  keyElement.type = "button";
197
-
198
268
  keyElement.dataset.key = key;
199
269
 
270
+ // FIX IMP-4: เพิ่ม aria-label บนทุก key
271
+ keyElement.setAttribute(
272
+ "aria-label",
273
+ key === " "
274
+ ? "spacebar"
275
+ : key === "backspace" || key === "Backspace"
276
+ ? "backspace"
277
+ : key,
278
+ );
279
+
200
280
  if (index >= row.length - 4) {
201
281
  keyElement.classList.add("concat-keys");
202
282
  }
203
283
 
204
284
  if (key === " ") {
205
- keyElement.className += " space";
206
- keyElement.innerHTML = 'ฺ';
285
+ keyElement.className += " spacebar";
286
+ keyElement.innerHTML = "";
207
287
  }
208
288
 
209
289
  if (key === "backspace" || key === "Backspace") {
210
290
  keyElement.className += " backspacew";
211
- keyElement.innerHTML = '<i class="fa fa-backspace"></i>ฺ';
291
+ keyElement.innerHTML = '<i class="fa fa-backspace"></i>';
292
+ }
293
+
294
+ // FIX WARN-2: restore active state หลัง re-render
295
+ if (key === "Caps 🄰" && this.capsLockActive) {
296
+ keyElement.classList.add("active", "bg-gray-400");
297
+ }
298
+ if (key === "Shift ⇧" && this.shiftActive) {
299
+ keyElement.classList.add("active", "bg-gray-400");
212
300
  }
213
301
 
214
302
  keyElement.onclick = (e) => {
@@ -230,16 +318,16 @@ export class VirtualKeyboard {
230
318
  this.container.innerHTML = "";
231
319
  this.container.appendChild(keyboard);
232
320
 
321
+ // FIX BUG-2: ใช้ bound reference แทน bind() inline
233
322
  keyboard.addEventListener("mousedown", (event) => this.startDrag(event));
234
323
  }
235
324
 
236
- // [2]
237
- async handleKeyPress(keyPressed) {
325
+ // FIX BUG-3 & WARN-5: เปลี่ยนเป็น sync, จัดการ isVirtualKeyboardActive อย่างถูกต้อง
326
+ handleKeyPress(keyPressed) {
238
327
  if (!this.currentInput) return;
239
-
240
- // Set flag to indicate virtual keyboard is active
328
+
241
329
  this.isVirtualKeyboardActive = true;
242
-
330
+
243
331
  const start = this.currentInput.selectionStart;
244
332
  const end = this.currentInput.selectionEnd;
245
333
  const buffer = this.getCurrentBuffer();
@@ -248,9 +336,7 @@ export class VirtualKeyboard {
248
336
  const isShiftActive = this.shiftActive;
249
337
 
250
338
  const convertToCorrectCase = (char) => {
251
- if (isCapsActive || isShiftActive) {
252
- return char.toUpperCase();
253
- }
339
+ if (isCapsActive || isShiftActive) return char.toUpperCase();
254
340
  return char.toLowerCase();
255
341
  };
256
342
 
@@ -260,71 +346,32 @@ export class VirtualKeyboard {
260
346
  }
261
347
 
262
348
  if (keyPressed === "scr" || keyPressed === "scrambled") {
263
- if (this.currentLayout === "en") {
264
- if (this.isScrambled) {
265
- this.unscrambleKeys();
266
- } else {
267
- this.scrambleEnglishKeys();
268
- }
269
- this.isScrambled = !this.isScrambled;
270
- document
271
- .querySelectorAll('.key[data-key="scrambled"]')
272
- .forEach((key) => {
273
- key.classList.toggle("active", this.isScrambled);
274
- });
275
- }
276
-
277
- if (this.currentLayout === "th") {
278
- if (this.isScrambled) {
279
- this.unscrambleKeys();
280
- } else {
281
- this.scrambleThaiKeys();
282
- }
349
+ const layoutScrambleMap = {
350
+ en: () =>
351
+ this.isScrambled ? this.unscrambleKeys() : this.scrambleEnglishKeys(),
352
+ th: () =>
353
+ this.isScrambled ? this.unscrambleKeys() : this.scrambleThaiKeys(),
354
+ numpad: () =>
355
+ this.isScrambled ? this.unscrambleKeys() : this.scrambleKeyboard(),
356
+ symbols: () =>
357
+ this.isScrambled ? this.unscrambleKeys() : this.scrambleSymbols(),
358
+ full: () =>
359
+ this.isScrambled
360
+ ? this.unscrambleKeys()
361
+ : this.scrambleFullLayoutKeys(),
362
+ };
363
+
364
+ const handler = layoutScrambleMap[this.currentLayout];
365
+ if (handler) {
366
+ handler();
283
367
  this.isScrambled = !this.isScrambled;
284
368
  document
285
- .querySelectorAll('.key[data-key="scrambled"]')
369
+ .querySelectorAll('.key[data-key="scr"], .key[data-key="scrambled"]')
286
370
  .forEach((key) => {
287
371
  key.classList.toggle("active", this.isScrambled);
288
372
  });
289
373
  }
290
374
 
291
- if (this.currentLayout === "numpad") {
292
- if (this.isScrambled) {
293
- this.unscrambleKeys();
294
- } else {
295
- this.scrambleKeyboard();
296
- }
297
- this.isScrambled = !this.isScrambled;
298
- document.querySelectorAll('.key[data-key="scr"]').forEach((key) => {
299
- key.classList.toggle("active", this.isScrambled);
300
- });
301
- }
302
-
303
- if (this.currentLayout === "symbols") {
304
- if (this.isScrambled) {
305
- this.unscrambleKeys();
306
- } else {
307
- this.scrambleSymbols();
308
- }
309
- this.isScrambled = !this.isScrambled;
310
- document.querySelectorAll('.key[data-key="scr"]').forEach((key) => {
311
- key.classList.toggle("active", this.isScrambled);
312
- });
313
- }
314
-
315
- if (this.currentLayout === "full") {
316
- if (this.isScrambled) {
317
- this.unscrambleKeys();
318
- } else {
319
- this.scrambleFullLayoutKeys();
320
- }
321
- this.isScrambled = !this.isScrambled;
322
- document
323
- .querySelectorAll('.key[data-key="scrambled"]')
324
- .forEach((key) => {
325
- key.classList.toggle("active", this.isScrambled);
326
- });
327
- }
328
375
  this.isVirtualKeyboardActive = false;
329
376
  return;
330
377
  }
@@ -337,11 +384,10 @@ export class VirtualKeyboard {
337
384
  this.currentInput.setSelectionRange(0, 0);
338
385
  this.currentInput.focus();
339
386
  }, 5);
340
- // Skip the general cursor repositioning for navigation keys
341
387
  this.isVirtualKeyboardActive = false;
342
388
  return;
343
389
 
344
- case "END":
390
+ case "END": {
345
391
  const bufferLengthEnd = this.getCurrentBuffer().length;
346
392
  this.currentInput.setSelectionRange(bufferLengthEnd, bufferLengthEnd);
347
393
  this.currentInput.focus();
@@ -349,9 +395,9 @@ export class VirtualKeyboard {
349
395
  this.currentInput.setSelectionRange(bufferLengthEnd, bufferLengthEnd);
350
396
  this.currentInput.focus();
351
397
  }, 5);
352
- // Skip the general cursor repositioning for navigation keys
353
398
  this.isVirtualKeyboardActive = false;
354
399
  return;
400
+ }
355
401
 
356
402
  case "Backspace":
357
403
  case "backspace":
@@ -361,12 +407,11 @@ export class VirtualKeyboard {
361
407
  const newCursorPos = start - 1;
362
408
  this.updateDisplayedValue(false);
363
409
  this.currentInput.setSelectionRange(newCursorPos, newCursorPos);
364
- } else {
410
+ } else if (start !== end) {
365
411
  const newBuffer = buffer.slice(0, start) + buffer.slice(end);
366
412
  this.setCurrentBuffer(newBuffer);
367
- const newCursorPos = start;
368
413
  this.updateDisplayedValue(false);
369
- this.currentInput.setSelectionRange(newCursorPos, newCursorPos);
414
+ this.currentInput.setSelectionRange(start, start);
370
415
  }
371
416
  break;
372
417
 
@@ -374,24 +419,22 @@ export class VirtualKeyboard {
374
419
  if (start === end && start < buffer.length) {
375
420
  const newBuffer = buffer.slice(0, start) + buffer.slice(end + 1);
376
421
  this.setCurrentBuffer(newBuffer);
377
- const newCursorPos = start;
378
422
  this.updateDisplayedValue(false);
379
- this.currentInput.setSelectionRange(newCursorPos, newCursorPos);
380
- } else {
423
+ this.currentInput.setSelectionRange(start, start);
424
+ } else if (start !== end) {
381
425
  const newBuffer = buffer.slice(0, start) + buffer.slice(end);
382
426
  this.setCurrentBuffer(newBuffer);
383
- const newCursorPos = start;
384
427
  this.updateDisplayedValue(false);
385
- this.currentInput.setSelectionRange(newCursorPos, newCursorPos);
428
+ this.currentInput.setSelectionRange(start, start);
386
429
  }
387
430
  break;
388
431
 
389
432
  case "Space":
390
- await this.insertText(" ");
433
+ this.insertText(" ");
391
434
  break;
392
435
 
393
436
  case "Tab ↹":
394
- await this.insertText("\t");
437
+ this.insertText("\t");
395
438
  break;
396
439
 
397
440
  case "Enter":
@@ -401,12 +444,17 @@ export class VirtualKeyboard {
401
444
  const newCursorPos = start + 1;
402
445
  this.updateDisplayedValue(false);
403
446
  this.currentInput.setSelectionRange(newCursorPos, newCursorPos);
404
- } else if (this.currentInput.tagName === "INPUT" || this.currentInput.type === "password" || this.currentInput.type === "text" ) {
447
+ } else if (
448
+ this.currentInput.tagName === "INPUT" ||
449
+ this.currentInput.type === "password" ||
450
+ this.currentInput.type === "text"
451
+ ) {
405
452
  if (this.currentInput.form) {
406
453
  const submitButton = this.currentInput.form.querySelector(
407
- 'input[type="submit"], button[type="submit"], button[type="button"], button[onclick]'
454
+ 'input[type="submit"], button[type="submit"], button[type="button"], button[onclick]',
408
455
  );
409
- if (submitButton) submitButton.click(); else this.currentInput.form.submit();
456
+ if (submitButton) submitButton.click();
457
+ else this.currentInput.form.submit();
410
458
  } else {
411
459
  const newBuffer = buffer + "\n";
412
460
  this.setCurrentBuffer(newBuffer);
@@ -423,213 +471,245 @@ export class VirtualKeyboard {
423
471
  this.toggleShift();
424
472
  break;
425
473
 
426
- case "←":
474
+ case "←": {
427
475
  const leftNewPos = Math.max(0, start - 1);
428
476
  this.currentInput.setSelectionRange(leftNewPos, leftNewPos);
429
- break;
477
+ this.isVirtualKeyboardActive = false;
478
+ return;
479
+ }
430
480
 
431
- case "→":
481
+ case "→": {
432
482
  const bufferLength = this.getCurrentBuffer().length;
433
483
  const rightNewPos = Math.min(bufferLength, start + 1);
434
484
  this.currentInput.setSelectionRange(rightNewPos, rightNewPos);
435
- break;
485
+ this.isVirtualKeyboardActive = false;
486
+ return;
487
+ }
436
488
 
437
489
  case "↑":
438
- case "↓":
439
- // Use the actual buffer content instead of displayed value
490
+ case "↓": {
440
491
  const actualText = this.getCurrentBuffer();
441
492
  const actualLines = actualText.substring(0, start).split("\n");
442
493
  const actualCurrentLineIndex = actualLines.length - 1;
443
494
  const actualCurrentLine = actualLines[actualCurrentLineIndex];
444
- const actualColumnIndex = start - actualText.lastIndexOf("\n", start - 1) - 1;
495
+ const actualColumnIndex =
496
+ start - actualText.lastIndexOf("\n", start - 1) - 1;
445
497
 
446
498
  if (keyPressed === "↑" && actualCurrentLineIndex > 0) {
447
- // Move to previous line
448
499
  const prevLineLength = actualLines[actualCurrentLineIndex - 1].length;
449
- const upNewPos = start - actualCurrentLine.length - 1 - Math.max(0, actualColumnIndex - prevLineLength);
450
- this.currentInput.setSelectionRange(Math.max(0, upNewPos), Math.max(0, upNewPos));
500
+ const upNewPos =
501
+ start -
502
+ actualCurrentLine.length -
503
+ 1 -
504
+ Math.max(0, actualColumnIndex - prevLineLength);
505
+ this.currentInput.setSelectionRange(
506
+ Math.max(0, upNewPos),
507
+ Math.max(0, upNewPos),
508
+ );
451
509
  } else if (keyPressed === "↓") {
452
- // Move to next line
453
510
  const remainingText = actualText.substring(start);
454
511
  const nextLineBreak = remainingText.indexOf("\n");
455
-
456
512
  if (nextLineBreak !== -1) {
457
- // There is a next line
458
- const textAfterNextLineBreak = remainingText.substring(nextLineBreak + 1);
513
+ const textAfterNextLineBreak = remainingText.substring(
514
+ nextLineBreak + 1,
515
+ );
459
516
  const nextLineEnd = textAfterNextLineBreak.indexOf("\n");
460
- const nextLineLength = nextLineEnd === -1 ? textAfterNextLineBreak.length : nextLineEnd;
461
-
462
- const downNewPos = start + nextLineBreak + 1 + Math.min(actualColumnIndex, nextLineLength);
517
+ const nextLineLength =
518
+ nextLineEnd === -1 ? textAfterNextLineBreak.length : nextLineEnd;
519
+ const downNewPos =
520
+ start +
521
+ nextLineBreak +
522
+ 1 +
523
+ Math.min(actualColumnIndex, nextLineLength);
463
524
  this.currentInput.setSelectionRange(downNewPos, downNewPos);
464
525
  }
465
- // If no next line, do nothing (cursor stays at current position)
466
526
  }
467
-
527
+
468
528
  this.currentInput.focus();
469
- // Skip the general cursor repositioning for arrow keys
470
529
  this.isVirtualKeyboardActive = false;
471
- break;
530
+ return;
531
+ }
472
532
 
473
- default:
474
- const textvalue = await convertToCorrectCase(keyPressed);
475
- await this.insertText(textvalue);
533
+ default: {
534
+ const textvalue = convertToCorrectCase(keyPressed);
535
+ this.insertText(textvalue);
536
+ break;
537
+ }
476
538
  }
477
539
 
478
540
  if (isShiftActive && !isCapsActive) this.toggleShift();
479
541
 
480
542
  this.currentInput.focus();
481
543
 
544
+ // FIX BUG-3: reset flag หลัง setTimeout เพื่อป้องกัน real keyboard listener เข้ามาแทรก
482
545
  setTimeout(() => {
483
- this.currentInput.focus();
484
- const cursorPos = this.currentInput.selectionStart;
485
- this.currentInput.setSelectionRange(cursorPos, cursorPos);
546
+ if (this.currentInput) {
547
+ this.currentInput.focus();
548
+ const cursorPos = this.currentInput.selectionStart;
549
+ this.currentInput.setSelectionRange(cursorPos, cursorPos);
550
+ }
551
+ this.isVirtualKeyboardActive = false;
486
552
  }, 0);
487
-
553
+
488
554
  this.currentInput.focus();
489
555
  const event = new Event("input", { bubbles: true });
490
556
  this.currentInput.dispatchEvent(event);
491
-
492
- // Reset the flag after virtual keyboard operation is complete
493
- this.isVirtualKeyboardActive = false;
494
557
  }
495
558
 
496
- // [3]
497
- async insertText(text) {
559
+ insertText(text) {
498
560
  const start = this.currentInput.selectionStart;
499
561
  const end = this.currentInput.selectionEnd;
500
- const textvalue = text;
501
562
 
502
563
  const buffer = this.getCurrentBuffer();
503
- const newBuffer = buffer.slice(0, start) + textvalue + buffer.slice(end);
564
+ const newBuffer = buffer.slice(0, start) + text + buffer.slice(end);
504
565
  this.setCurrentBuffer(newBuffer);
505
-
506
- const newCursorPos = start + textvalue.length;
507
-
566
+
567
+ const newCursorPos = start + text.length;
508
568
  this.updateDisplayedValue(false);
509
-
510
569
  this.currentInput.setSelectionRange(newCursorPos, newCursorPos);
511
570
  }
512
571
 
513
- // Helper: get current plaintext buffer for active input
514
572
  getCurrentBuffer() {
515
573
  if (!this.currentInput) return "";
516
574
  return this.inputPlaintextBuffers.get(this.currentInput) || "";
517
575
  }
518
576
 
519
- // Helper: set buffer and update encrypted dataset
577
+ getBufferForInput(inputElement) {
578
+ if (!inputElement) return "";
579
+ const bufferedValue = this.inputPlaintextBuffers.get(inputElement);
580
+ if (typeof bufferedValue === "string") {
581
+ return bufferedValue;
582
+ }
583
+ return inputElement.value || "";
584
+ }
585
+
520
586
  setCurrentBuffer(newBuffer) {
521
587
  if (!this.currentInput) return;
522
588
  this.inputPlaintextBuffers.set(this.currentInput, newBuffer);
523
- this.updateDomEncrypted(this.currentInput, newBuffer);
524
589
  }
525
590
 
526
- // Helper: reflect masked value in the visible input/textarea
527
591
  updateDisplayedValue(preserveCursor = true) {
528
592
  if (!this.currentInput) return;
529
593
  const buffer = this.getCurrentBuffer();
530
594
  const isPassword = this.currentInput.type === "password";
531
595
  const maskChar = "•";
532
-
533
- // Store current cursor position only if we want to preserve it
596
+
534
597
  const currentStart = preserveCursor ? this.currentInput.selectionStart : 0;
535
598
  const currentEnd = preserveCursor ? this.currentInput.selectionEnd : 0;
536
-
537
- this.currentInput.value = isPassword ? maskChar.repeat(buffer.length) : buffer;
538
-
539
- // Restore cursor position after updating value only if preserving
599
+
600
+ this.currentInput.value = isPassword
601
+ ? maskChar.repeat(buffer.length)
602
+ : buffer;
603
+
540
604
  if (preserveCursor) {
541
605
  this.currentInput.setSelectionRange(currentStart, currentEnd);
542
606
  }
543
607
  }
544
608
 
545
- // Handle real keyboard keydown events to track actual input before masking
546
609
  handleRealKeyboardKeydown(input, event) {
547
610
  if (!input) return;
548
-
611
+
549
612
  const key = event.key;
550
613
  const start = input.selectionStart;
551
614
  const end = input.selectionEnd;
552
615
  const buffer = this.getCurrentBuffer();
553
-
554
- // Skip modifier keys and special keys
555
- if (event.ctrlKey || event.altKey || event.metaKey ||
556
- ['Shift', 'Control', 'Alt', 'Meta', 'CapsLock', 'Tab', 'Escape',
557
- 'ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'End',
558
- 'F1', 'F2', 'F3', 'F4', 'F5', 'F6', 'F7', 'F8', 'F9', 'F10', 'F11', 'F12'].includes(key)) {
616
+
617
+ if (event.ctrlKey || event.altKey || event.metaKey) {
618
+ // FIX BUG-6: handle Ctrl+A
619
+ if ((event.ctrlKey || event.metaKey) && key === "a") {
620
+ // Allow default selection no buffer change needed
621
+ return;
622
+ }
623
+ // FIX BUG-6: handle Ctrl+Z (undo) — best-effort: เราไม่เก็บ history จึง ignore
624
+ return;
625
+ }
626
+
627
+ if (
628
+ [
629
+ "Shift",
630
+ "Control",
631
+ "Alt",
632
+ "Meta",
633
+ "CapsLock",
634
+ "Tab",
635
+ "Escape",
636
+ "ArrowLeft",
637
+ "ArrowRight",
638
+ "ArrowUp",
639
+ "ArrowDown",
640
+ "Home",
641
+ "End",
642
+ "F1",
643
+ "F2",
644
+ "F3",
645
+ "F4",
646
+ "F5",
647
+ "F6",
648
+ "F7",
649
+ "F8",
650
+ "F9",
651
+ "F10",
652
+ "F11",
653
+ "F12",
654
+ ].includes(key)
655
+ ) {
559
656
  return;
560
657
  }
561
-
658
+
562
659
  let newBuffer = buffer;
563
-
564
- if (key === 'Backspace') {
660
+
661
+ if (key === "Backspace") {
565
662
  if (start === end && start > 0) {
566
- // Single character deletion
567
663
  newBuffer = buffer.slice(0, start - 1) + buffer.slice(start);
568
664
  } else if (start !== end) {
569
- // Range deletion
570
665
  newBuffer = buffer.slice(0, start) + buffer.slice(end);
571
666
  }
572
-
573
- // Prevent default and update manually
667
+
574
668
  event.preventDefault();
575
669
  this.setCurrentBuffer(newBuffer);
576
670
  this.updateDisplayedValue(false);
577
-
578
- // Set cursor position
671
+
579
672
  const newCursorPos = start === end ? Math.max(0, start - 1) : start;
580
673
  setTimeout(() => {
581
674
  input.setSelectionRange(newCursorPos, newCursorPos);
582
675
  input.focus();
583
676
  }, 0);
584
-
585
- } else if (key === 'Delete') {
677
+ } else if (key === "Delete") {
586
678
  if (start === end && start < buffer.length) {
587
- // Single character deletion forward
588
679
  newBuffer = buffer.slice(0, start) + buffer.slice(start + 1);
589
680
  } else if (start !== end) {
590
- // Range deletion
591
681
  newBuffer = buffer.slice(0, start) + buffer.slice(end);
592
682
  }
593
-
594
- // Prevent default and update manually
683
+
595
684
  event.preventDefault();
596
685
  this.setCurrentBuffer(newBuffer);
597
686
  this.updateDisplayedValue(false);
598
-
599
- // Set cursor position
687
+
600
688
  setTimeout(() => {
601
689
  input.setSelectionRange(start, start);
602
690
  input.focus();
603
691
  }, 0);
604
-
605
- } else if (key === 'Enter') {
606
- if (input.tagName === 'TEXTAREA') {
607
- newBuffer = buffer.slice(0, start) + '\n' + buffer.slice(end);
608
-
609
- // Prevent default and update manually
692
+ } else if (key === "Enter") {
693
+ if (input.tagName === "TEXTAREA") {
694
+ newBuffer = buffer.slice(0, start) + "\n" + buffer.slice(end);
695
+
610
696
  event.preventDefault();
611
697
  this.setCurrentBuffer(newBuffer);
612
698
  this.updateDisplayedValue(false);
613
-
614
- // Set cursor position
699
+
615
700
  const newCursorPos = start + 1;
616
701
  setTimeout(() => {
617
702
  input.setSelectionRange(newCursorPos, newCursorPos);
618
703
  input.focus();
619
704
  }, 0);
620
705
  }
621
- // For input fields, let the default behavior handle form submission
622
-
623
706
  } else if (key.length === 1) {
624
- // Regular character input
625
707
  newBuffer = buffer.slice(0, start) + key + buffer.slice(end);
626
-
627
- // Prevent default and update manually
708
+
628
709
  event.preventDefault();
629
710
  this.setCurrentBuffer(newBuffer);
630
711
  this.updateDisplayedValue(false);
631
-
632
- // Set cursor position
712
+
633
713
  const newCursorPos = start + key.length;
634
714
  setTimeout(() => {
635
715
  input.setSelectionRange(newCursorPos, newCursorPos);
@@ -638,153 +718,200 @@ export class VirtualKeyboard {
638
718
  }
639
719
  }
640
720
 
641
- // Handle real keyboard input and encrypt it (simplified version)
721
+ // FIX BUG-4: handleRealKeyboardInput ทำหน้าที่ sync buffer เมื่อ autocomplete / IME
642
722
  handleRealKeyboardInput(input, event) {
643
- if (!input) return;
644
-
645
- // For most cases, the keydown handler already took care of updating the buffer
646
- // This method now just ensures the encryption is up to date
647
- const currentBuffer = this.getCurrentBuffer();
648
- this.updateDomEncrypted(input, currentBuffer);
649
- }
723
+ if (!input || !event) return;
724
+
725
+ // กรณี autocomplete, IME composition, หรือ browser fill ที่ไม่ผ่าน keydown
726
+ // sync จาก actual value กลับมาใส่ buffer
727
+ if (
728
+ event.inputType &&
729
+ event.inputType.startsWith("insertReplacementText")
730
+ ) {
731
+ this.inputPlaintextBuffers.set(input, input.value);
732
+ return;
733
+ }
650
734
 
651
- // Helper: encrypt and store ciphertext in DOM attribute
652
- updateDomEncrypted(input, plaintext) {
653
- try {
654
- const crypto = window.VKCrypto;
655
-
656
- if (crypto && typeof crypto.encrypt === "function") {
657
- const cipher = plaintext.length ? crypto.encrypt(plaintext) : "";
658
- input.dataset.encrypted = cipher;
659
- } else {
660
- // No encryption function available — don't store anything
661
- console.warn("Encryption unavailable. Not storing plaintext.");
662
- input.dataset.encrypted = "";
663
- }
664
-
665
- } catch (err) {
666
- console.error("Failed to encrypt input:", err);
667
- input.dataset.encrypted = "";
735
+ // IME composition end
736
+ if (
737
+ event.isComposing === false &&
738
+ event.inputType === "insertCompositionText"
739
+ ) {
740
+ this.inputPlaintextBuffers.set(input, input.value);
668
741
  }
669
742
  }
670
743
 
671
- // [4]
672
744
  unscrambleKeys() {
673
- this.keys = this.originalKeys;
674
745
  this.render();
675
746
  }
676
747
 
677
- // [5]
678
- toggleCapsLock() {
679
- this.capsLockActive = !this.capsLockActive;
680
-
681
- document.querySelectorAll('.key[data-key="Caps 🄰"]').forEach((key) => {
682
- key.classList.toggle("active", this.capsLockActive);
683
- key.classList.toggle("bg-gray-400", this.capsLockActive);
684
- });
748
+ // FIX BUG-1: รวม toggleCapsLock และ toggleShift ให้มี updateKeyContent เดียว
749
+ // ลบเมธอด updateKeyContent ที่ซ้ำกันออก และใช้เมธอดเดียวที่รับ parameter
750
+ _updateAllKeyContent(isActive) {
751
+ const layoutMap = {
752
+ th: this.ThaiAlphabetShift,
753
+ en: this.EngAlphabetShift,
754
+ full: this.FullAlphabetShift,
755
+ };
756
+ const layout = layoutMap[this.currentLayout];
685
757
 
686
758
  document.querySelectorAll(".key").forEach((key) => {
687
- const isLetter = key.dataset.key.length === 1 && /[a-zA-Zก-๙]/.test(key.dataset.key);
759
+ const isLetter =
760
+ key.dataset.key &&
761
+ key.dataset.key.length === 1 &&
762
+ /[a-zA-Zก-๙]/.test(key.dataset.key);
688
763
 
689
764
  if (isLetter) {
690
- key.textContent = this.capsLockActive
765
+ key.textContent = isActive
691
766
  ? key.dataset.key.toUpperCase()
692
767
  : key.dataset.key.toLowerCase();
693
768
  }
694
- this.updateKeyContent(key, this.capsLockActive);
769
+
770
+ if (!layout) return;
771
+
772
+ const currentChar = key.textContent.trim();
773
+ if (isActive && layout[currentChar]) {
774
+ key.textContent = layout[currentChar];
775
+ key.dataset.key = layout[currentChar];
776
+ } else if (!isActive && Object.values(layout).includes(currentChar)) {
777
+ const originalKey = Object.keys(layout).find(
778
+ (k) => layout[k] === currentChar,
779
+ );
780
+ if (originalKey) {
781
+ key.textContent = originalKey;
782
+ key.dataset.key = originalKey;
783
+ }
784
+ }
695
785
  });
696
786
  }
697
- updateKeyContent(key, capsLockActive) {
698
- const currentChar = key.textContent.trim();
699
787
 
700
- const layouts = {
701
- th: this.ThaiAlphabetShift,
702
- en: this.EngAlphabetShift,
703
- enSc: this.EngAlphabetShift,
704
- full: this.FullAlphabetShift,
705
- };
706
- const layout = layouts[this.currentLayout];
707
-
708
- if (!layout) return;
709
-
710
- if (capsLockActive && layout[currentChar]) {
711
- key.textContent = layout[currentChar];
712
- key.dataset.key = layout[currentChar];
713
- } else if (!capsLockActive && Object.values(layout).includes(currentChar)) {
714
- const originalKey = Object.keys(layout).find((k) => layout[k] === currentChar);
715
- if (originalKey) {
716
- key.textContent = originalKey;
717
- key.dataset.key = originalKey;
718
- }
719
- }
788
+ toggleCapsLock() {
789
+ this.capsLockActive = !this.capsLockActive;
790
+
791
+ document.querySelectorAll('.key[data-key="Caps 🄰"]').forEach((key) => {
792
+ key.classList.toggle("active", this.capsLockActive);
793
+ key.classList.toggle("bg-gray-400", this.capsLockActive);
794
+ });
795
+
796
+ // FIX BUG-1: ใช้ _updateAllKeyContent แทน updateKeyContent ที่ซ้ำกัน
797
+ this._updateAllKeyContent(this.capsLockActive);
720
798
  }
721
799
 
722
- // [6]
723
800
  toggleShift() {
724
801
  if (this.capsLockActive) {
725
802
  return this.toggleCapsLock();
726
803
  }
727
804
 
728
805
  this.shiftActive = !this.shiftActive;
806
+
729
807
  document.querySelectorAll('.key[data-key="Shift ⇧"]').forEach((key) => {
730
808
  key.classList.toggle("active", this.shiftActive);
731
809
  key.classList.toggle("bg-gray-400", this.shiftActive);
732
810
  });
733
-
734
- document.querySelectorAll(".key").forEach((key) => {
735
- const isLetter = key.dataset.key.length === 1 && /[a-zA-Zก-๙]/.test(key.dataset.key);
736
-
737
- if (isLetter) {
738
- key.textContent = this.shiftActive
739
- ? key.dataset.key.toUpperCase()
740
- : key.dataset.key.toLowerCase();
741
- }
742
- this.updateKeyContent(key, this.shiftActive);
743
- });
744
- }
745
- updateKeyContent(key, shiftActive) {
746
- const currentChar = key.textContent.trim();
747
-
748
- const layouts = {
749
- th: this.ThaiAlphabetShift,
750
- en: this.EngAlphabetShift,
751
- enSc: this.EngAlphabetShift,
752
- full: this.FullAlphabetShift,
753
- };
754
- const layout = layouts[this.currentLayout];
755
-
756
- if (!layout) return;
757
-
758
- if (shiftActive && layout[currentChar]) {
759
- key.textContent = layout[currentChar];
760
- key.dataset.key = layout[currentChar];
761
- } else if (!shiftActive && Object.values(layout).includes(currentChar)) {
762
- const originalKey = Object.keys(layout).find((k) => layout[k] === currentChar);
763
- if (originalKey) {
764
- key.textContent = originalKey;
765
- key.dataset.key = originalKey;
766
- }
767
- }
811
+
812
+ // FIX BUG-1: ใช้ _updateAllKeyContent แทน updateKeyContent ที่ซ้ำกัน
813
+ this._updateAllKeyContent(this.shiftActive);
768
814
  }
769
815
 
770
- // [7]
771
- EngAlphabetShift = { "`": "~", 1: "!", 2: "@", 3: "#", 4: "$", 5: "%", 6: "^", 7: "&", 8: "*", 9: "(", 0: ")", "-": "_", "=": "+", "[": "{", "]": "}", "\\": "|", ";": ":", "'": '"', ",": "<", ".": ">", "/": "?", };
772
- ThaiAlphabetShift = { _: "%", ๅ: "+", "/": "๑", "-": "๒", ภ: "๓", ถ: "๔", "ุ": "ู", "ึ": "฿", ค: "๕", ต: "๖", จ: "๗", ข: "๘", ช: "๙", ๆ: "๐", ไ: '"', ำ: "ฎ", พ: "ฑ", ะ: "ธ", "ั": "ํ", "ี": "๋", ร: "ณ", น: "ฯ", ย: "ญ", บ: "ฐ", ล: ",", ฃ: "ฅ", ฟ: "ฤ", ห: "ฆ", ก: "ฏ", ด: "โ", เ: "ฌ", "้": "็", "่": "๋", า: "ษ", ส: "ศ", ว: "ซ", ง: ".", ผ: "(", ป: ")", แ: "ฉ", อ: "ฮ", "ิ": "ฺ", "ื": "์", ท: "?", ม: "ฒ", ใ: "ฬ", ฝ: "ฦ"};
773
- FullAlphabetShift = { "[": "{", "]": "}", "\\": "|", ";": ":", "'": '"', ",": "<", ".": ">", "/": "?", };
816
+ EngAlphabetShift = {
817
+ "`": "~",
818
+ 1: "!",
819
+ 2: "@",
820
+ 3: "#",
821
+ 4: "$",
822
+ 5: "%",
823
+ 6: "^",
824
+ 7: "&",
825
+ 8: "*",
826
+ 9: "(",
827
+ 0: ")",
828
+ "-": "_",
829
+ "=": "+",
830
+ "[": "{",
831
+ "]": "}",
832
+ "\\": "|",
833
+ ";": ":",
834
+ "'": '"',
835
+ ",": "<",
836
+ ".": ">",
837
+ "/": "?",
838
+ };
839
+
840
+ ThaiAlphabetShift = {
841
+ _: "%",
842
+ ๅ: "+",
843
+ "/": "๑",
844
+ "-": "๒",
845
+ ภ: "๓",
846
+ ถ: "๔",
847
+ "ุ": "ู",
848
+ "ึ": "฿",
849
+ ค: "๕",
850
+ ต: "๖",
851
+ จ: "๗",
852
+ ข: "๘",
853
+ ช: "๙",
854
+ ๆ: "๐",
855
+ ไ: '"',
856
+ ำ: "ฎ",
857
+ พ: "ฑ",
858
+ ะ: "ธ",
859
+ "ั": "ํ",
860
+ "ี": "๋",
861
+ ร: "ณ",
862
+ น: "ฯ",
863
+ ย: "ญ",
864
+ บ: "ฐ",
865
+ ล: ",",
866
+ ฃ: "ฅ",
867
+ ฟ: "ฤ",
868
+ ห: "ฆ",
869
+ ก: "ฏ",
870
+ ด: "โ",
871
+ เ: "ฌ",
872
+ "้": "็",
873
+ "่": "๋",
874
+ า: "ษ",
875
+ ส: "ศ",
876
+ ว: "ซ",
877
+ ง: ".",
878
+ ผ: "(",
879
+ ป: ")",
880
+ แ: "ฉ",
881
+ อ: "ฮ",
882
+ "ิ": "ฺ",
883
+ "ื": "์",
884
+ ท: "?",
885
+ ม: "ฒ",
886
+ ใ: "ฬ",
887
+ ฝ: "ฦ",
888
+ };
889
+
890
+ FullAlphabetShift = {
891
+ "[": "{",
892
+ "]": "}",
893
+ "\\": "|",
894
+ ";": ":",
895
+ "'": '"',
896
+ ",": "<",
897
+ ".": ">",
898
+ "/": "?",
899
+ };
774
900
 
775
- // [8]
776
901
  toggle() {
777
902
  this.isVisible = !this.isVisible;
778
903
  this.render();
779
904
  }
780
905
 
781
- // [9]
906
+ // FIX WARN-1: reset isScrambled และ shift/caps เมื่อเปลี่ยน layout
782
907
  changeLayout(layout) {
783
908
  this.currentLayout = layout;
909
+ this.isScrambled = false;
910
+ this.shiftActive = false;
911
+ this.capsLockActive = false;
784
912
  this.render();
785
913
  }
786
914
 
787
- // [10]
788
915
  shuffleArray(array) {
789
916
  for (let i = array.length - 1; i > 0; i--) {
790
917
  const j = Math.floor(Math.random() * (i + 1));
@@ -792,64 +919,74 @@ export class VirtualKeyboard {
792
919
  }
793
920
  }
794
921
 
795
- // [11]
922
+ // FIX BUG-7: แก้ CSS selector — เพิ่มวงเล็บปิด ) และ fix data-key case
796
923
  scrambleKeyboard() {
797
924
  const keys = document.querySelectorAll(
798
- ":not([data-key='std']):not([data-key='scr']).key:not([data-key=backspace]"
925
+ ":not([data-key='std']):not([data-key='scr']).key:not([data-key='backspace']):not([data-key='Backspace'])",
799
926
  );
800
927
  const numbers = "1234567890-=+%/*.()".split("");
801
928
  this.shuffleArray(numbers);
802
929
  keys.forEach((key, index) => {
803
- key.textContent = numbers[index];
804
- key.dataset.key = numbers[index];
930
+ if (index < numbers.length) {
931
+ key.textContent = numbers[index];
932
+ key.dataset.key = numbers[index];
933
+ }
805
934
  });
806
935
  }
807
936
 
808
- // [12]
809
937
  scrambleEnglishKeys() {
810
938
  const keys = document.querySelectorAll(
811
- ".key:not([data-key='scrambled']):not([data-key='std']):not([data-key='scr']):not([data-key=' ']):not([data-key='Backspace']):not([data-key='Caps 🄰']):not([data-key='Shift ⇧']):not([data-key='Enter']):not([data-key='Tab ↹'])"
939
+ ".key:not([data-key='scrambled']):not([data-key='std']):not([data-key='scr']):not([data-key=' ']):not([data-key='Backspace']):not([data-key='Caps 🄰']):not([data-key='Shift ⇧']):not([data-key='Enter']):not([data-key='Tab ↹'])",
812
940
  );
813
- const englishAlphabet = "abcdefghijklmnopqrstuvwxyz/.,';\\][`1234567890-=".split("");
941
+ const englishAlphabet =
942
+ "abcdefghijklmnopqrstuvwxyz/.,';\\][`1234567890-=".split("");
814
943
  this.shuffleArray(englishAlphabet);
815
944
  keys.forEach((key, index) => {
816
- key.textContent = englishAlphabet[index];
817
- key.dataset.key = englishAlphabet[index];
945
+ if (index < englishAlphabet.length) {
946
+ key.textContent = englishAlphabet[index];
947
+ key.dataset.key = englishAlphabet[index];
948
+ }
818
949
  });
819
950
  }
820
951
 
821
- // [13]
822
952
  scrambleThaiKeys() {
823
953
  const keys = document.querySelectorAll(
824
- ".key:not([data-key='scrambled']):not([data-key='scr']):not([data-key='Backspace']):not([data-key='Caps 🄰']):not([data-key='Shift ⇧']):not([data-key='Enter']):not([data-key=' ']):not([data-key='Tab ↹'])"
954
+ ".key:not([data-key='scrambled']):not([data-key='scr']):not([data-key='Backspace']):not([data-key='Caps 🄰']):not([data-key='Shift ⇧']):not([data-key='Enter']):not([data-key=' ']):not([data-key='Tab ↹'])",
825
955
  );
826
- const ThaiAlphabet = '_ๅ/-ภถุึคตจขชๆไำพะัีรนยบลฃฟหกดเ้่าสวงผปแอิืทมใฝ%+๑๒๓๔ู฿๕๖๗๘๙๐"ฎฑธํ๊ณฯญฐ,ฅฤฆฏโฌ็๋ษศซ.ฦฬฒ?์ฺฮฉ)('.split("");
956
+ const ThaiAlphabet =
957
+ '_ๅ/-ภถุึคตจขชๆไำพะัีรนยบลฃฟหกดเ้่าสวงผปแอิืทมใฝ%+๑๒๓๔ู฿๕๖๗๘๙๐"ฎฑธํ๊ณฯญฐ,ฅฤฆฏโฌ็๋ษศซ.ฦฬฒ?์ฺฮฉ)('.split(
958
+ "",
959
+ );
827
960
  this.shuffleArray(ThaiAlphabet);
828
961
  keys.forEach((key, index) => {
829
- key.textContent = ThaiAlphabet[index];
830
- key.dataset.key = ThaiAlphabet[index];
962
+ if (index < ThaiAlphabet.length) {
963
+ key.textContent = ThaiAlphabet[index];
964
+ key.dataset.key = ThaiAlphabet[index];
965
+ }
831
966
  });
832
967
  }
833
968
 
834
- // []
969
+ // FIX BUG-7: แก้ CSS selector ของ scrambleSymbols
835
970
  scrambleSymbols() {
836
971
  const keys = document.querySelectorAll(
837
- ":not([data-key='std']):not([data-key='scr']).key:not([data-key=backspace]"
972
+ ":not([data-key='std']):not([data-key='scr']).key:not([data-key='backspace']):not([data-key='Backspace'])",
838
973
  );
839
- const numbers = "@#$%^&*()_+~`{}|\\:'<>?/[]±§¶!€£¥¢©®™℅‰†".split("");
840
- this.shuffleArray(numbers);
974
+ const symbols = "@#$%^&*()_+~`{}|\\:'<>?/[]±§¶!€£¥¢©®™℅‰†".split("");
975
+ this.shuffleArray(symbols);
841
976
  keys.forEach((key, index) => {
842
- key.textContent = numbers[index];
843
- key.dataset.key = numbers[index];
977
+ if (index < symbols.length) {
978
+ key.textContent = symbols[index];
979
+ key.dataset.key = symbols[index];
980
+ }
844
981
  });
845
982
  }
846
983
 
847
-
848
984
  scrambleFullLayoutKeys() {
849
985
  const keys = document.querySelectorAll(
850
- ".key:not(.concat-keys):not([data-key='scrambled']):not([data-key='std']):not([data-key='scr']):not([data-key=' ']):not([data-key='Backspace']):not([data-key='Caps 🄰']):not([data-key='Shift ⇧']):not([data-key='Enter']):not([data-key='Tab ↹'])"
986
+ ".key:not(.concat-keys):not([data-key='scrambled']):not([data-key='std']):not([data-key='scr']):not([data-key=' ']):not([data-key='Backspace']):not([data-key='Caps 🄰']):not([data-key='Shift ⇧']):not([data-key='Enter']):not([data-key='Tab ↹'])",
851
987
  );
852
- const englishChars = "abcdefghijklmnopqrstuvwxyz1234567890;'\\/][`,.-=".split("");
988
+ const englishChars =
989
+ "abcdefghijklmnopqrstuvwxyz1234567890;'\\/][`,.-=".split("");
853
990
  this.shuffleArray(englishChars);
854
991
  keys.forEach((key, index) => {
855
992
  if (index < englishChars.length) {
@@ -859,20 +996,24 @@ export class VirtualKeyboard {
859
996
  });
860
997
  }
861
998
 
862
- // [14] จัดการการลากคีย์บอร์ด
999
+ // FIX BUG-2: เก็บ bound reference และ cleanup ให้ถูกต้อง
863
1000
  startDrag(event) {
864
1001
  this.isDragging = true;
865
- this.offsetX = event.clientX - document.getElementById("keyboard").offsetLeft;
866
- this.offsetY = event.clientY - document.getElementById("keyboard").offsetTop;
1002
+ this.offsetX =
1003
+ event.clientX - document.getElementById("keyboard").offsetLeft;
1004
+ this.offsetY =
1005
+ event.clientY - document.getElementById("keyboard").offsetTop;
867
1006
 
868
- document.addEventListener("mousemove", this.drag.bind(this));
869
- document.addEventListener("mouseup", () => {
870
- this.isDragging = false;
871
- document.removeEventListener("mousemove", this.drag.bind(this));
872
- });
1007
+ document.addEventListener("mousemove", this._boundDrag);
1008
+ document.addEventListener("mouseup", this._boundStopDrag);
1009
+ }
1010
+
1011
+ stopDrag() {
1012
+ this.isDragging = false;
1013
+ document.removeEventListener("mousemove", this._boundDrag);
1014
+ document.removeEventListener("mouseup", this._boundStopDrag);
873
1015
  }
874
1016
 
875
- // [15]
876
1017
  drag(event) {
877
1018
  if (this.isDragging) {
878
1019
  const keyboard = document.getElementById("keyboard");
@@ -880,4 +1021,4 @@ export class VirtualKeyboard {
880
1021
  keyboard.style.top = `${event.clientY - this.offsetY}px`;
881
1022
  }
882
1023
  }
883
- }
1024
+ }