js_lis 2.0.8 → 3.0.1

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