js_lis 2.0.7 → 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,30 +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
- this.handleRealKeyboardInput(target);
162
+ this.handleRealKeyboardInput(target, e);
83
163
  }
84
164
  }
85
165
  });
86
166
 
87
- // Add real keyboard arrow key support
88
167
  document.addEventListener("keydown", (e) => {
89
168
  const target = e.target;
90
- if ((target.tagName === "INPUT" || target.tagName === "TEXTAREA") && target === this.currentInput) {
91
- 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)) {
183
+ this.isVirtualKeyboardActive = false;
184
+ }
185
+
186
+ if (!this.isVirtualKeyboardActive) {
187
+ this.handleRealKeyboardKeydown(target, e);
188
+ }
189
+
190
+ if (navKeys.includes(e.code)) {
92
191
  setTimeout(() => {
93
192
  target.focus();
94
193
  }, 0);
@@ -109,29 +208,14 @@ export class VirtualKeyboard {
109
208
 
110
209
  this.currentInput = inputElement;
111
210
  this.currentInput.classList.add("keyboard-active");
112
-
113
211
  this.currentInput.focus();
114
-
115
- // Ensure buffer initialized for this input
212
+
116
213
  if (!this.inputPlaintextBuffers.has(this.currentInput)) {
117
- const ds = this.currentInput.dataset ? this.currentInput.dataset.encrypted : undefined;
118
- let initial = "";
119
- try {
120
- if (ds && window.VKCrypto && typeof window.VKCrypto.decrypt === "function") {
121
- initial = window.VKCrypto.decrypt(ds) || "";
122
- } else {
123
- initial = this.currentInput.value || "";
124
- }
125
- } catch (_) {
126
- initial = this.currentInput.value || "";
127
- }
214
+ const initial = this.currentInput.value || "";
128
215
  this.inputPlaintextBuffers.set(this.currentInput, initial);
129
- this.updateDomEncrypted(this.currentInput, initial);
130
216
  }
131
- // Sync visible with buffer (masking for password)
132
217
  this.updateDisplayedValue();
133
-
134
- // Only move cursor to end on initial focus, not during virtual keyboard operations
218
+
135
219
  if (moveToEnd) {
136
220
  setTimeout(() => {
137
221
  const length = this.currentInput.value.length;
@@ -140,7 +224,6 @@ export class VirtualKeyboard {
140
224
  }
141
225
  }
142
226
 
143
- // [1]
144
227
  render() {
145
228
  const keyboard = document.createElement("div");
146
229
  keyboard.className = `virtual-keyboard ${this.currentLayout}`;
@@ -156,10 +239,11 @@ export class VirtualKeyboard {
156
239
 
157
240
  const layoutSelector = document.createElement("select");
158
241
  layoutSelector.id = "layout-selector";
242
+ layoutSelector.setAttribute("aria-label", "Select keyboard layout");
159
243
  layoutSelector.onchange = (e) => this.changeLayout(e.target.value);
160
244
 
161
- const layouts = ["full", "en", "th", "numpad", "symbols"];
162
- layouts.forEach((layout) => {
245
+ const availableLayouts = ["full", "en", "th", "numpad", "symbols"];
246
+ availableLayouts.forEach((layout) => {
163
247
  const option = document.createElement("option");
164
248
  option.value = layout;
165
249
  option.innerText = this.getLayoutName(layout);
@@ -181,15 +265,25 @@ export class VirtualKeyboard {
181
265
  keyElement.className = "keyboard-key key";
182
266
  keyElement.textContent = key;
183
267
  keyElement.type = "button";
184
-
185
268
  keyElement.dataset.key = key;
186
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
+
187
280
  if (index >= row.length - 4) {
188
281
  keyElement.classList.add("concat-keys");
189
282
  }
190
283
 
191
- if (key === "Space") {
192
- keyElement.className += " space";
284
+ if (key === " ") {
285
+ keyElement.className += " spacebar";
286
+ keyElement.innerHTML = "";
193
287
  }
194
288
 
195
289
  if (key === "backspace" || key === "Backspace") {
@@ -197,6 +291,14 @@ export class VirtualKeyboard {
197
291
  keyElement.innerHTML = '<i class="fa fa-backspace"></i>';
198
292
  }
199
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");
300
+ }
301
+
200
302
  keyElement.onclick = (e) => {
201
303
  e.preventDefault();
202
304
  const keyPressed = keyElement.dataset.key || keyElement.textContent;
@@ -216,16 +318,16 @@ export class VirtualKeyboard {
216
318
  this.container.innerHTML = "";
217
319
  this.container.appendChild(keyboard);
218
320
 
321
+ // FIX BUG-2: ใช้ bound reference แทน bind() inline
219
322
  keyboard.addEventListener("mousedown", (event) => this.startDrag(event));
220
323
  }
221
324
 
222
- // [2]
223
- async handleKeyPress(keyPressed) {
325
+ // FIX BUG-3 & WARN-5: เปลี่ยนเป็น sync, จัดการ isVirtualKeyboardActive อย่างถูกต้อง
326
+ handleKeyPress(keyPressed) {
224
327
  if (!this.currentInput) return;
225
-
226
- // Set flag to indicate virtual keyboard is active
328
+
227
329
  this.isVirtualKeyboardActive = true;
228
-
330
+
229
331
  const start = this.currentInput.selectionStart;
230
332
  const end = this.currentInput.selectionEnd;
231
333
  const buffer = this.getCurrentBuffer();
@@ -234,9 +336,7 @@ export class VirtualKeyboard {
234
336
  const isShiftActive = this.shiftActive;
235
337
 
236
338
  const convertToCorrectCase = (char) => {
237
- if (isCapsActive || isShiftActive) {
238
- return char.toUpperCase();
239
- }
339
+ if (isCapsActive || isShiftActive) return char.toUpperCase();
240
340
  return char.toLowerCase();
241
341
  };
242
342
 
@@ -246,71 +346,32 @@ export class VirtualKeyboard {
246
346
  }
247
347
 
248
348
  if (keyPressed === "scr" || keyPressed === "scrambled") {
249
- if (this.currentLayout === "en") {
250
- if (this.isScrambled) {
251
- this.unscrambleKeys();
252
- } else {
253
- this.scrambleEnglishKeys();
254
- }
255
- this.isScrambled = !this.isScrambled;
256
- document
257
- .querySelectorAll('.key[data-key="scrambled"]')
258
- .forEach((key) => {
259
- key.classList.toggle("active", this.isScrambled);
260
- });
261
- }
262
-
263
- if (this.currentLayout === "th") {
264
- if (this.isScrambled) {
265
- this.unscrambleKeys();
266
- } else {
267
- this.scrambleThaiKeys();
268
- }
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();
269
367
  this.isScrambled = !this.isScrambled;
270
368
  document
271
- .querySelectorAll('.key[data-key="scrambled"]')
369
+ .querySelectorAll('.key[data-key="scr"], .key[data-key="scrambled"]')
272
370
  .forEach((key) => {
273
371
  key.classList.toggle("active", this.isScrambled);
274
372
  });
275
373
  }
276
374
 
277
- if (this.currentLayout === "numpad") {
278
- if (this.isScrambled) {
279
- this.unscrambleKeys();
280
- } else {
281
- this.scrambleKeyboard();
282
- }
283
- this.isScrambled = !this.isScrambled;
284
- document.querySelectorAll('.key[data-key="scr"]').forEach((key) => {
285
- key.classList.toggle("active", this.isScrambled);
286
- });
287
- }
288
-
289
- if (this.currentLayout === "symbols") {
290
- if (this.isScrambled) {
291
- this.unscrambleKeys();
292
- } else {
293
- this.scrambleSymbols();
294
- }
295
- this.isScrambled = !this.isScrambled;
296
- document.querySelectorAll('.key[data-key="scr"]').forEach((key) => {
297
- key.classList.toggle("active", this.isScrambled);
298
- });
299
- }
300
-
301
- if (this.currentLayout === "full") {
302
- if (this.isScrambled) {
303
- this.unscrambleKeys();
304
- } else {
305
- this.scrambleFullLayoutKeys();
306
- }
307
- this.isScrambled = !this.isScrambled;
308
- document
309
- .querySelectorAll('.key[data-key="scrambled"]')
310
- .forEach((key) => {
311
- key.classList.toggle("active", this.isScrambled);
312
- });
313
- }
314
375
  this.isVirtualKeyboardActive = false;
315
376
  return;
316
377
  }
@@ -323,11 +384,10 @@ export class VirtualKeyboard {
323
384
  this.currentInput.setSelectionRange(0, 0);
324
385
  this.currentInput.focus();
325
386
  }, 5);
326
- // Skip the general cursor repositioning for navigation keys
327
387
  this.isVirtualKeyboardActive = false;
328
388
  return;
329
389
 
330
- case "END":
390
+ case "END": {
331
391
  const bufferLengthEnd = this.getCurrentBuffer().length;
332
392
  this.currentInput.setSelectionRange(bufferLengthEnd, bufferLengthEnd);
333
393
  this.currentInput.focus();
@@ -335,9 +395,9 @@ export class VirtualKeyboard {
335
395
  this.currentInput.setSelectionRange(bufferLengthEnd, bufferLengthEnd);
336
396
  this.currentInput.focus();
337
397
  }, 5);
338
- // Skip the general cursor repositioning for navigation keys
339
398
  this.isVirtualKeyboardActive = false;
340
399
  return;
400
+ }
341
401
 
342
402
  case "Backspace":
343
403
  case "backspace":
@@ -347,12 +407,11 @@ export class VirtualKeyboard {
347
407
  const newCursorPos = start - 1;
348
408
  this.updateDisplayedValue(false);
349
409
  this.currentInput.setSelectionRange(newCursorPos, newCursorPos);
350
- } else {
410
+ } else if (start !== end) {
351
411
  const newBuffer = buffer.slice(0, start) + buffer.slice(end);
352
412
  this.setCurrentBuffer(newBuffer);
353
- const newCursorPos = start;
354
413
  this.updateDisplayedValue(false);
355
- this.currentInput.setSelectionRange(newCursorPos, newCursorPos);
414
+ this.currentInput.setSelectionRange(start, start);
356
415
  }
357
416
  break;
358
417
 
@@ -360,24 +419,22 @@ export class VirtualKeyboard {
360
419
  if (start === end && start < buffer.length) {
361
420
  const newBuffer = buffer.slice(0, start) + buffer.slice(end + 1);
362
421
  this.setCurrentBuffer(newBuffer);
363
- const newCursorPos = start;
364
422
  this.updateDisplayedValue(false);
365
- this.currentInput.setSelectionRange(newCursorPos, newCursorPos);
366
- } else {
423
+ this.currentInput.setSelectionRange(start, start);
424
+ } else if (start !== end) {
367
425
  const newBuffer = buffer.slice(0, start) + buffer.slice(end);
368
426
  this.setCurrentBuffer(newBuffer);
369
- const newCursorPos = start;
370
427
  this.updateDisplayedValue(false);
371
- this.currentInput.setSelectionRange(newCursorPos, newCursorPos);
428
+ this.currentInput.setSelectionRange(start, start);
372
429
  }
373
430
  break;
374
431
 
375
432
  case "Space":
376
- await this.insertText(" ");
433
+ this.insertText(" ");
377
434
  break;
378
435
 
379
436
  case "Tab ↹":
380
- await this.insertText("\t");
437
+ this.insertText("\t");
381
438
  break;
382
439
 
383
440
  case "Enter":
@@ -387,12 +444,17 @@ export class VirtualKeyboard {
387
444
  const newCursorPos = start + 1;
388
445
  this.updateDisplayedValue(false);
389
446
  this.currentInput.setSelectionRange(newCursorPos, newCursorPos);
390
- } 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
+ ) {
391
452
  if (this.currentInput.form) {
392
453
  const submitButton = this.currentInput.form.querySelector(
393
- 'input[type="submit"], button[type="submit"], button[type="button"], button[onclick]'
454
+ 'input[type="submit"], button[type="submit"], button[type="button"], button[onclick]',
394
455
  );
395
- if (submitButton) submitButton.click(); else this.currentInput.form.submit();
456
+ if (submitButton) submitButton.click();
457
+ else this.currentInput.form.submit();
396
458
  } else {
397
459
  const newBuffer = buffer + "\n";
398
460
  this.setCurrentBuffer(newBuffer);
@@ -409,273 +471,447 @@ export class VirtualKeyboard {
409
471
  this.toggleShift();
410
472
  break;
411
473
 
412
- case "←":
474
+ case "←": {
413
475
  const leftNewPos = Math.max(0, start - 1);
414
476
  this.currentInput.setSelectionRange(leftNewPos, leftNewPos);
415
- break;
477
+ this.isVirtualKeyboardActive = false;
478
+ return;
479
+ }
416
480
 
417
- case "→":
481
+ case "→": {
418
482
  const bufferLength = this.getCurrentBuffer().length;
419
483
  const rightNewPos = Math.min(bufferLength, start + 1);
420
484
  this.currentInput.setSelectionRange(rightNewPos, rightNewPos);
421
- break;
485
+ this.isVirtualKeyboardActive = false;
486
+ return;
487
+ }
422
488
 
423
489
  case "↑":
424
- case "↓":
425
- // Use the actual buffer content instead of displayed value
490
+ case "↓": {
426
491
  const actualText = this.getCurrentBuffer();
427
492
  const actualLines = actualText.substring(0, start).split("\n");
428
493
  const actualCurrentLineIndex = actualLines.length - 1;
429
494
  const actualCurrentLine = actualLines[actualCurrentLineIndex];
430
- const actualColumnIndex = start - actualText.lastIndexOf("\n", start - 1) - 1;
495
+ const actualColumnIndex =
496
+ start - actualText.lastIndexOf("\n", start - 1) - 1;
431
497
 
432
498
  if (keyPressed === "↑" && actualCurrentLineIndex > 0) {
433
- // Move to previous line
434
499
  const prevLineLength = actualLines[actualCurrentLineIndex - 1].length;
435
- const upNewPos = start - actualCurrentLine.length - 1 - Math.max(0, actualColumnIndex - prevLineLength);
436
- 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
+ );
437
509
  } else if (keyPressed === "↓") {
438
- // Move to next line
439
510
  const remainingText = actualText.substring(start);
440
511
  const nextLineBreak = remainingText.indexOf("\n");
441
-
442
512
  if (nextLineBreak !== -1) {
443
- // There is a next line
444
- const textAfterNextLineBreak = remainingText.substring(nextLineBreak + 1);
513
+ const textAfterNextLineBreak = remainingText.substring(
514
+ nextLineBreak + 1,
515
+ );
445
516
  const nextLineEnd = textAfterNextLineBreak.indexOf("\n");
446
- const nextLineLength = nextLineEnd === -1 ? textAfterNextLineBreak.length : nextLineEnd;
447
-
448
- 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);
449
524
  this.currentInput.setSelectionRange(downNewPos, downNewPos);
450
525
  }
451
- // If no next line, do nothing (cursor stays at current position)
452
526
  }
453
-
527
+
454
528
  this.currentInput.focus();
455
- // Skip the general cursor repositioning for arrow keys
456
529
  this.isVirtualKeyboardActive = false;
457
- break;
530
+ return;
531
+ }
458
532
 
459
- default:
460
- const textvalue = await convertToCorrectCase(keyPressed);
461
- await this.insertText(textvalue);
533
+ default: {
534
+ const textvalue = convertToCorrectCase(keyPressed);
535
+ this.insertText(textvalue);
536
+ break;
537
+ }
462
538
  }
463
539
 
464
540
  if (isShiftActive && !isCapsActive) this.toggleShift();
465
541
 
466
542
  this.currentInput.focus();
467
543
 
544
+ // FIX BUG-3: reset flag หลัง setTimeout เพื่อป้องกัน real keyboard listener เข้ามาแทรก
468
545
  setTimeout(() => {
469
- this.currentInput.focus();
470
- const cursorPos = this.currentInput.selectionStart;
471
- 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;
472
552
  }, 0);
473
-
553
+
474
554
  this.currentInput.focus();
475
555
  const event = new Event("input", { bubbles: true });
476
556
  this.currentInput.dispatchEvent(event);
477
557
  }
478
558
 
479
- // [3]
480
- async insertText(text) {
559
+ insertText(text) {
481
560
  const start = this.currentInput.selectionStart;
482
561
  const end = this.currentInput.selectionEnd;
483
- const textvalue = text;
484
562
 
485
563
  const buffer = this.getCurrentBuffer();
486
- const newBuffer = buffer.slice(0, start) + textvalue + buffer.slice(end);
564
+ const newBuffer = buffer.slice(0, start) + text + buffer.slice(end);
487
565
  this.setCurrentBuffer(newBuffer);
488
-
489
- const newCursorPos = start + textvalue.length;
490
-
566
+
567
+ const newCursorPos = start + text.length;
491
568
  this.updateDisplayedValue(false);
492
-
493
569
  this.currentInput.setSelectionRange(newCursorPos, newCursorPos);
494
570
  }
495
571
 
496
- // Helper: get current plaintext buffer for active input
497
572
  getCurrentBuffer() {
498
573
  if (!this.currentInput) return "";
499
574
  return this.inputPlaintextBuffers.get(this.currentInput) || "";
500
575
  }
501
576
 
502
- // 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
+
503
586
  setCurrentBuffer(newBuffer) {
504
587
  if (!this.currentInput) return;
505
588
  this.inputPlaintextBuffers.set(this.currentInput, newBuffer);
506
- this.updateDomEncrypted(this.currentInput, newBuffer);
507
589
  }
508
590
 
509
- // Helper: reflect masked value in the visible input/textarea
510
591
  updateDisplayedValue(preserveCursor = true) {
511
592
  if (!this.currentInput) return;
512
593
  const buffer = this.getCurrentBuffer();
513
594
  const isPassword = this.currentInput.type === "password";
514
595
  const maskChar = "•";
515
-
516
- // Store current cursor position only if we want to preserve it
596
+
517
597
  const currentStart = preserveCursor ? this.currentInput.selectionStart : 0;
518
598
  const currentEnd = preserveCursor ? this.currentInput.selectionEnd : 0;
519
-
520
- this.currentInput.value = isPassword ? maskChar.repeat(buffer.length) : buffer;
521
-
522
- // Restore cursor position after updating value only if preserving
599
+
600
+ this.currentInput.value = isPassword
601
+ ? maskChar.repeat(buffer.length)
602
+ : buffer;
603
+
523
604
  if (preserveCursor) {
524
605
  this.currentInput.setSelectionRange(currentStart, currentEnd);
525
606
  }
526
607
  }
527
608
 
528
- // Handle real keyboard input and encrypt it
529
- handleRealKeyboardInput(input) {
609
+ handleRealKeyboardKeydown(input, event) {
530
610
  if (!input) return;
531
-
532
- // Get the current value from the input
533
- const currentValue = input.value;
534
-
535
- // Update the plaintext buffer with the real keyboard input
536
- this.inputPlaintextBuffers.set(input, currentValue);
537
-
538
- // Encrypt and store in data-encrypted attribute
539
- this.updateDomEncrypted(input, currentValue);
540
- }
541
611
 
542
- // Helper: encrypt and store ciphertext in DOM attribute
543
- updateDomEncrypted(input, plaintext) {
544
- try {
545
- const crypto = window.VKCrypto;
546
-
547
- if (crypto && typeof crypto.encrypt === "function") {
548
- const cipher = plaintext.length ? crypto.encrypt(plaintext) : "";
549
- input.dataset.encrypted = cipher;
550
- } else {
551
- // No encryption function available — don't store anything
552
- console.warn("Encryption unavailable. Not storing plaintext.");
553
- input.dataset.encrypted = "";
612
+ const key = event.key;
613
+ const start = input.selectionStart;
614
+ const end = input.selectionEnd;
615
+ const buffer = this.getCurrentBuffer();
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
+ ) {
656
+ return;
657
+ }
658
+
659
+ let newBuffer = buffer;
660
+
661
+ if (key === "Backspace") {
662
+ if (start === end && start > 0) {
663
+ newBuffer = buffer.slice(0, start - 1) + buffer.slice(start);
664
+ } else if (start !== end) {
665
+ newBuffer = buffer.slice(0, start) + buffer.slice(end);
666
+ }
667
+
668
+ event.preventDefault();
669
+ this.setCurrentBuffer(newBuffer);
670
+ this.updateDisplayedValue(false);
671
+
672
+ const newCursorPos = start === end ? Math.max(0, start - 1) : start;
673
+ setTimeout(() => {
674
+ input.setSelectionRange(newCursorPos, newCursorPos);
675
+ input.focus();
676
+ }, 0);
677
+ } else if (key === "Delete") {
678
+ if (start === end && start < buffer.length) {
679
+ newBuffer = buffer.slice(0, start) + buffer.slice(start + 1);
680
+ } else if (start !== end) {
681
+ newBuffer = buffer.slice(0, start) + buffer.slice(end);
682
+ }
683
+
684
+ event.preventDefault();
685
+ this.setCurrentBuffer(newBuffer);
686
+ this.updateDisplayedValue(false);
687
+
688
+ setTimeout(() => {
689
+ input.setSelectionRange(start, start);
690
+ input.focus();
691
+ }, 0);
692
+ } else if (key === "Enter") {
693
+ if (input.tagName === "TEXTAREA") {
694
+ newBuffer = buffer.slice(0, start) + "\n" + buffer.slice(end);
695
+
696
+ event.preventDefault();
697
+ this.setCurrentBuffer(newBuffer);
698
+ this.updateDisplayedValue(false);
699
+
700
+ const newCursorPos = start + 1;
701
+ setTimeout(() => {
702
+ input.setSelectionRange(newCursorPos, newCursorPos);
703
+ input.focus();
704
+ }, 0);
554
705
  }
555
-
556
- } catch (err) {
557
- console.error("Failed to encrypt input:", err);
558
- input.dataset.encrypted = "";
706
+ } else if (key.length === 1) {
707
+ newBuffer = buffer.slice(0, start) + key + buffer.slice(end);
708
+
709
+ event.preventDefault();
710
+ this.setCurrentBuffer(newBuffer);
711
+ this.updateDisplayedValue(false);
712
+
713
+ const newCursorPos = start + key.length;
714
+ setTimeout(() => {
715
+ input.setSelectionRange(newCursorPos, newCursorPos);
716
+ input.focus();
717
+ }, 0);
718
+ }
719
+ }
720
+
721
+ // FIX BUG-4: handleRealKeyboardInput ทำหน้าที่ sync buffer เมื่อ autocomplete / IME
722
+ handleRealKeyboardInput(input, event) {
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
+ }
734
+
735
+ // IME composition end
736
+ if (
737
+ event.isComposing === false &&
738
+ event.inputType === "insertCompositionText"
739
+ ) {
740
+ this.inputPlaintextBuffers.set(input, input.value);
559
741
  }
560
742
  }
561
743
 
562
- // [4]
563
744
  unscrambleKeys() {
564
- this.keys = this.originalKeys;
565
745
  this.render();
566
746
  }
567
747
 
568
- // [5]
569
- toggleCapsLock() {
570
- this.capsLockActive = !this.capsLockActive;
571
-
572
- document.querySelectorAll('.key[data-key="Caps 🄰"]').forEach((key) => {
573
- key.classList.toggle("active", this.capsLockActive);
574
- key.classList.toggle("bg-gray-400", this.capsLockActive);
575
- });
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];
576
757
 
577
758
  document.querySelectorAll(".key").forEach((key) => {
578
- 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);
579
763
 
580
764
  if (isLetter) {
581
- key.textContent = this.capsLockActive
765
+ key.textContent = isActive
582
766
  ? key.dataset.key.toUpperCase()
583
767
  : key.dataset.key.toLowerCase();
584
768
  }
585
- 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
+ }
586
785
  });
587
786
  }
588
- updateKeyContent(key, capsLockActive) {
589
- const currentChar = key.textContent.trim();
590
787
 
591
- const layouts = {
592
- th: this.ThaiAlphabetShift,
593
- en: this.EngAlphabetShift,
594
- enSc: this.EngAlphabetShift,
595
- full: this.FullAlphabetShift,
596
- };
597
- const layout = layouts[this.currentLayout];
598
-
599
- if (!layout) return;
600
-
601
- if (capsLockActive && layout[currentChar]) {
602
- key.textContent = layout[currentChar];
603
- key.dataset.key = layout[currentChar];
604
- } else if (!capsLockActive && Object.values(layout).includes(currentChar)) {
605
- const originalKey = Object.keys(layout).find((k) => layout[k] === currentChar);
606
- if (originalKey) {
607
- key.textContent = originalKey;
608
- key.dataset.key = originalKey;
609
- }
610
- }
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);
611
798
  }
612
799
 
613
- // [6]
614
800
  toggleShift() {
615
801
  if (this.capsLockActive) {
616
802
  return this.toggleCapsLock();
617
803
  }
618
804
 
619
805
  this.shiftActive = !this.shiftActive;
806
+
620
807
  document.querySelectorAll('.key[data-key="Shift ⇧"]').forEach((key) => {
621
808
  key.classList.toggle("active", this.shiftActive);
622
809
  key.classList.toggle("bg-gray-400", this.shiftActive);
623
810
  });
624
-
625
- document.querySelectorAll(".key").forEach((key) => {
626
- const isLetter = key.dataset.key.length === 1 && /[a-zA-Zก-๙]/.test(key.dataset.key);
627
-
628
- if (isLetter) {
629
- key.textContent = this.shiftActive
630
- ? key.dataset.key.toUpperCase()
631
- : key.dataset.key.toLowerCase();
632
- }
633
- this.updateKeyContent(key, this.shiftActive);
634
- });
635
- }
636
- updateKeyContent(key, shiftActive) {
637
- const currentChar = key.textContent.trim();
638
-
639
- const layouts = {
640
- th: this.ThaiAlphabetShift,
641
- en: this.EngAlphabetShift,
642
- enSc: this.EngAlphabetShift,
643
- full: this.FullAlphabetShift,
644
- };
645
- const layout = layouts[this.currentLayout];
646
-
647
- if (!layout) return;
648
-
649
- if (shiftActive && layout[currentChar]) {
650
- key.textContent = layout[currentChar];
651
- key.dataset.key = layout[currentChar];
652
- } else if (!shiftActive && Object.values(layout).includes(currentChar)) {
653
- const originalKey = Object.keys(layout).find((k) => layout[k] === currentChar);
654
- if (originalKey) {
655
- key.textContent = originalKey;
656
- key.dataset.key = originalKey;
657
- }
658
- }
811
+
812
+ // FIX BUG-1: ใช้ _updateAllKeyContent แทน updateKeyContent ที่ซ้ำกัน
813
+ this._updateAllKeyContent(this.shiftActive);
659
814
  }
660
815
 
661
- // [7]
662
- EngAlphabetShift = { "`": "~", 1: "!", 2: "@", 3: "#", 4: "$", 5: "%", 6: "^", 7: "&", 8: "*", 9: "(", 0: ")", "-": "_", "=": "+", "[": "{", "]": "}", "\\": "|", ";": ":", "'": '"', ",": "<", ".": ">", "/": "?", };
663
- ThaiAlphabetShift = { _: "%", ๅ: "+", "/": "๑", "-": "๒", ภ: "๓", ถ: "๔", "ุ": "ู", "ึ": "฿", ค: "๕", ต: "๖", จ: "๗", ข: "๘", ช: "๙", ๆ: "๐", ไ: '"', ำ: "ฎ", พ: "ฑ", ะ: "ธ", "ั": "ํ", "ี": "๋", ร: "ณ", น: "ฯ", ย: "ญ", บ: "ฐ", ล: ",", ฃ: "ฅ", ฟ: "ฤ", ห: "ฆ", ก: "ฏ", ด: "โ", เ: "ฌ", "้": "็", "่": "๋", า: "ษ", ส: "ศ", ว: "ซ", ง: ".", ผ: "(", ป: ")", แ: "ฉ", อ: "ฮ", "ิ": "ฺ", "ื": "์", ท: "?", ม: "ฒ", ใ: "ฬ", ฝ: "ฦ"};
664
- 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
+ };
665
900
 
666
- // [8]
667
901
  toggle() {
668
902
  this.isVisible = !this.isVisible;
669
903
  this.render();
670
904
  }
671
905
 
672
- // [9]
906
+ // FIX WARN-1: reset isScrambled และ shift/caps เมื่อเปลี่ยน layout
673
907
  changeLayout(layout) {
674
908
  this.currentLayout = layout;
909
+ this.isScrambled = false;
910
+ this.shiftActive = false;
911
+ this.capsLockActive = false;
675
912
  this.render();
676
913
  }
677
914
 
678
- // [10]
679
915
  shuffleArray(array) {
680
916
  for (let i = array.length - 1; i > 0; i--) {
681
917
  const j = Math.floor(Math.random() * (i + 1));
@@ -683,64 +919,74 @@ export class VirtualKeyboard {
683
919
  }
684
920
  }
685
921
 
686
- // [11]
922
+ // FIX BUG-7: แก้ CSS selector — เพิ่มวงเล็บปิด ) และ fix data-key case
687
923
  scrambleKeyboard() {
688
924
  const keys = document.querySelectorAll(
689
- ":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'])",
690
926
  );
691
927
  const numbers = "1234567890-=+%/*.()".split("");
692
928
  this.shuffleArray(numbers);
693
929
  keys.forEach((key, index) => {
694
- key.textContent = numbers[index];
695
- key.dataset.key = numbers[index];
930
+ if (index < numbers.length) {
931
+ key.textContent = numbers[index];
932
+ key.dataset.key = numbers[index];
933
+ }
696
934
  });
697
935
  }
698
936
 
699
- // [12]
700
937
  scrambleEnglishKeys() {
701
938
  const keys = document.querySelectorAll(
702
- ".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 ↹'])",
703
940
  );
704
- const englishAlphabet = "abcdefghijklmnopqrstuvwxyz/.,';\\][`1234567890-=".split("");
941
+ const englishAlphabet =
942
+ "abcdefghijklmnopqrstuvwxyz/.,';\\][`1234567890-=".split("");
705
943
  this.shuffleArray(englishAlphabet);
706
944
  keys.forEach((key, index) => {
707
- key.textContent = englishAlphabet[index];
708
- key.dataset.key = englishAlphabet[index];
945
+ if (index < englishAlphabet.length) {
946
+ key.textContent = englishAlphabet[index];
947
+ key.dataset.key = englishAlphabet[index];
948
+ }
709
949
  });
710
950
  }
711
951
 
712
- // [13]
713
952
  scrambleThaiKeys() {
714
953
  const keys = document.querySelectorAll(
715
- ".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 ↹'])",
716
955
  );
717
- const ThaiAlphabet = '_ๅ/-ภถุึคตจขชๆไำพะัีรนยบลฃฟหกดเ้่าสวงผปแอิืทมใฝ%+๑๒๓๔ู฿๕๖๗๘๙๐"ฎฑธํ๊ณฯญฐ,ฅฤฆฏโฌ็๋ษศซ.ฦฬฒ?์ฺฮฉ)('.split("");
956
+ const ThaiAlphabet =
957
+ '_ๅ/-ภถุึคตจขชๆไำพะัีรนยบลฃฟหกดเ้่าสวงผปแอิืทมใฝ%+๑๒๓๔ู฿๕๖๗๘๙๐"ฎฑธํ๊ณฯญฐ,ฅฤฆฏโฌ็๋ษศซ.ฦฬฒ?์ฺฮฉ)('.split(
958
+ "",
959
+ );
718
960
  this.shuffleArray(ThaiAlphabet);
719
961
  keys.forEach((key, index) => {
720
- key.textContent = ThaiAlphabet[index];
721
- key.dataset.key = ThaiAlphabet[index];
962
+ if (index < ThaiAlphabet.length) {
963
+ key.textContent = ThaiAlphabet[index];
964
+ key.dataset.key = ThaiAlphabet[index];
965
+ }
722
966
  });
723
967
  }
724
968
 
725
- // []
969
+ // FIX BUG-7: แก้ CSS selector ของ scrambleSymbols
726
970
  scrambleSymbols() {
727
971
  const keys = document.querySelectorAll(
728
- ":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'])",
729
973
  );
730
- const numbers = "@#$%^&*()_+~`{}|\\:'<>?/[]±§¶!€£¥¢©®™℅‰†".split("");
731
- this.shuffleArray(numbers);
974
+ const symbols = "@#$%^&*()_+~`{}|\\:'<>?/[]±§¶!€£¥¢©®™℅‰†".split("");
975
+ this.shuffleArray(symbols);
732
976
  keys.forEach((key, index) => {
733
- key.textContent = numbers[index];
734
- key.dataset.key = numbers[index];
977
+ if (index < symbols.length) {
978
+ key.textContent = symbols[index];
979
+ key.dataset.key = symbols[index];
980
+ }
735
981
  });
736
982
  }
737
983
 
738
-
739
984
  scrambleFullLayoutKeys() {
740
985
  const keys = document.querySelectorAll(
741
- ".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 ↹'])",
742
987
  );
743
- const englishChars = "abcdefghijklmnopqrstuvwxyz1234567890;'\\/][`,.-=".split("");
988
+ const englishChars =
989
+ "abcdefghijklmnopqrstuvwxyz1234567890;'\\/][`,.-=".split("");
744
990
  this.shuffleArray(englishChars);
745
991
  keys.forEach((key, index) => {
746
992
  if (index < englishChars.length) {
@@ -750,20 +996,24 @@ export class VirtualKeyboard {
750
996
  });
751
997
  }
752
998
 
753
- // [14] จัดการการลากคีย์บอร์ด
999
+ // FIX BUG-2: เก็บ bound reference และ cleanup ให้ถูกต้อง
754
1000
  startDrag(event) {
755
1001
  this.isDragging = true;
756
- this.offsetX = event.clientX - document.getElementById("keyboard").offsetLeft;
757
- 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;
758
1006
 
759
- document.addEventListener("mousemove", this.drag.bind(this));
760
- document.addEventListener("mouseup", () => {
761
- this.isDragging = false;
762
- document.removeEventListener("mousemove", this.drag.bind(this));
763
- });
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);
764
1015
  }
765
1016
 
766
- // [15]
767
1017
  drag(event) {
768
1018
  if (this.isDragging) {
769
1019
  const keyboard = document.getElementById("keyboard");
@@ -771,4 +1021,4 @@ export class VirtualKeyboard {
771
1021
  keyboard.style.top = `${event.clientY - this.offsetY}px`;
772
1022
  }
773
1023
  }
774
- }
1024
+ }