js_lis 2.0.1 → 2.0.3

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.
Files changed (3) hide show
  1. package/VirtualKeyboard.js +827 -624
  2. package/main.js +43 -10
  3. package/package.json +1 -1
@@ -1,625 +1,828 @@
1
- import { layouts } from "./layouts.js";
2
-
3
- export class VirtualKeyboard {
4
- constructor() {
5
- this.currentLayout = "full";
6
- this.isVisible = false;
7
- this.container = document.getElementById("keyboard-container");
8
- this.currentInput = null;
9
- this.layouts = layouts;
10
- this.isDragging = false;
11
- this.offsetX = 0;
12
- this.offsetY = 0;
13
- this.shiftActive = false;
14
- this.capsLockActive = false;
15
- this.isScrambled = false;
16
-
17
- this.initialize();
18
- }
19
-
20
- async initialize() {
21
- try {
22
- this.render();
23
- this.initializeInputListeners();
24
- console.log("VirtualKeyboard initialized successfully.");
25
- } catch (error) {
26
- console.error("Error initializing VirtualKeyboard:", error);
27
- }
28
- }
29
-
30
- getLayoutName(layout) {
31
- switch (layout) {
32
- case "full":
33
- return "Full Keyboard";
34
- case "en":
35
- return "English Keyboard";
36
- case "th":
37
- return "Thai keyboard";
38
- case "numpad":
39
- return "Numpad Keyboard";
40
- case "symbols":
41
- return "Symbols Keyboard";
42
- default:
43
- return "Unknown Layout";
44
- }
45
- }
46
-
47
- initializeInputListeners() {
48
- document.addEventListener("click", (e) => {
49
- const target = e.target;
50
- if (target.tagName === "INPUT" || target.tagName === "TEXTAREA") {
51
- this.setCurrentInput(target);
52
- }
53
- });
54
-
55
- document.addEventListener(
56
- "focus",
57
- (e) => {
58
- const target = e.target;
59
- if (target.tagName === "INPUT" || target.tagName === "TEXTAREA") {
60
- this.setCurrentInput(target);
61
- }
62
- },
63
- true
64
- );
65
-
66
- const toggle = document.getElementById("toggle");
67
- if (toggle) {
68
- toggle.addEventListener("click", this.toggle.bind(this));
69
- }
70
- }
71
-
72
- setCurrentInput(inputElement) {
73
- if (this.currentInput) {
74
- this.currentInput.classList.remove("keyboard-active");
75
- }
76
-
77
- this.currentInput = inputElement;
78
- this.currentInput.classList.add("keyboard-active");
79
- }
80
-
81
- // [1]
82
- render() {
83
- const keyboard = document.createElement("div");
84
- keyboard.className = `virtual-keyboard ${this.currentLayout}`;
85
- keyboard.style.display = this.isVisible ? "block" : "none";
86
- keyboard.id = "keyboard";
87
-
88
- const controlsContainer = document.createElement("div");
89
- controlsContainer.className = "controls";
90
- controlsContainer.style.display = "flex";
91
- controlsContainer.style.justifyContent = "center";
92
- controlsContainer.style.alignItems = "center";
93
- controlsContainer.style.marginBottom = "10px";
94
-
95
- const layoutSelector = document.createElement("select");
96
- layoutSelector.id = "layout-selector";
97
- layoutSelector.onchange = (e) => this.changeLayout(e.target.value);
98
-
99
- const layouts = ["full", "en", "th", "numpad", "symbols"];
100
- layouts.forEach((layout) => {
101
- const option = document.createElement("option");
102
- option.value = layout;
103
- option.innerText = this.getLayoutName(layout);
104
- layoutSelector.appendChild(option);
105
- });
106
- layoutSelector.value = this.currentLayout;
107
- controlsContainer.appendChild(layoutSelector);
108
-
109
- keyboard.appendChild(controlsContainer);
110
-
111
- const layout = this.layouts[this.currentLayout];
112
-
113
- layout.forEach((row) => {
114
- const rowElement = document.createElement("div");
115
- rowElement.className = "keyboard-row";
116
-
117
- row.forEach((key, index) => {
118
- const keyElement = document.createElement("button");
119
- keyElement.className = "keyboard-key key";
120
- keyElement.textContent = key;
121
- keyElement.type = "button";
122
-
123
- keyElement.dataset.key = key;
124
-
125
- if (index >= row.length - 4) {
126
- keyElement.classList.add("concat-keys");
127
- }
128
-
129
- if (key === "Space") {
130
- keyElement.className += " space";
131
- }
132
-
133
- if (key === "backspace" || key === "Backspace") {
134
- keyElement.className += " backspacew";
135
- keyElement.innerHTML = '<i class="fa fa-backspace"></i>';
136
- }
137
-
138
- keyElement.onclick = (e) => {
139
- e.preventDefault();
140
- const keyPressed = keyElement.dataset.key || keyElement.textContent;
141
- if (keyPressed) {
142
- this.handleKeyPress(keyPressed);
143
- } else {
144
- console.error("The key element does not have a valid key value.");
145
- }
146
- };
147
-
148
- rowElement.appendChild(keyElement);
149
- });
150
-
151
- keyboard.appendChild(rowElement);
152
- });
153
-
154
- this.container.innerHTML = "";
155
- this.container.appendChild(keyboard);
156
-
157
- keyboard.addEventListener("mousedown", (event) => this.startDrag(event));
158
- }
159
-
160
- // [2]
161
- async handleKeyPress(keyPressed) {
162
- if (!this.currentInput) return;
163
- const start = this.currentInput.selectionStart;
164
- const end = this.currentInput.selectionEnd;
165
- const value = this.currentInput.value;
166
-
167
- const isCapsActive = this.capsLockActive;
168
- const isShiftActive = this.shiftActive;
169
-
170
- const convertToCorrectCase = (char) => {
171
- if (isCapsActive || isShiftActive) {
172
- return char.toUpperCase();
173
- }
174
- return char.toLowerCase();
175
- };
176
-
177
- if (!keyPressed) return console.error("Invalid key pressed.");
178
-
179
- if (keyPressed === "scr" || keyPressed === "scrambled") {
180
- if (this.currentLayout === "en") {
181
- if (this.isScrambled) {
182
- this.unscrambleKeys();
183
- } else {
184
- this.scrambleEnglishKeys();
185
- }
186
- this.isScrambled = !this.isScrambled;
187
- document
188
- .querySelectorAll('.key[data-key="scrambled"]')
189
- .forEach((key) => {
190
- key.classList.toggle("active", this.isScrambled);
191
- });
192
- }
193
-
194
- if (this.currentLayout === "th") {
195
- if (this.isScrambled) {
196
- this.unscrambleKeys();
197
- } else {
198
- this.scrambleThaiKeys();
199
- }
200
- this.isScrambled = !this.isScrambled;
201
- document
202
- .querySelectorAll('.key[data-key="scrambled"]')
203
- .forEach((key) => {
204
- key.classList.toggle("active", this.isScrambled);
205
- });
206
- }
207
-
208
- if (this.currentLayout === "numpad") {
209
- if (this.isScrambled) {
210
- this.unscrambleKeys();
211
- } else {
212
- this.scrambleKeyboard();
213
- }
214
- this.isScrambled = !this.isScrambled;
215
- document.querySelectorAll('.key[data-key="scr"]').forEach((key) => {
216
- key.classList.toggle("active", this.isScrambled);
217
- });
218
- }
219
-
220
- if (this.currentLayout === "symbols") {
221
- if (this.isScrambled) {
222
- this.unscrambleKeys();
223
- } else {
224
- this.scrambleSymbols();
225
- }
226
- this.isScrambled = !this.isScrambled;
227
- document.querySelectorAll('.key[data-key="scr"]').forEach((key) => {
228
- key.classList.toggle("active", this.isScrambled);
229
- });
230
- }
231
- return;
232
- }
233
-
234
- switch (keyPressed) {
235
- case "Esc":
236
- const modals = document.querySelectorAll(".modal");
237
- if (modals.length === 0) {
238
- document.exitFullscreen();
239
- console.warn("No modals found to close.");
240
- }
241
- modals.forEach((modal) => {
242
- modal.classList.add("hidden");
243
- });
244
- break;
245
-
246
- case "F1":
247
- break;
248
-
249
- case "F2":
250
- // เปิดโหมดแก้ไขสำหรับ element ที่เลือก
251
- const activeElement = document.activeElement;
252
- if (activeElement && activeElement.contentEditable !== undefined) {
253
- activeElement.contentEditable = true;
254
- activeElement.focus();
255
- console.log(activeElement.contentEditable);
256
- } else {
257
- console.warn("No editable element found.");
258
- }
259
- break;
260
-
261
- case "F3":
262
- // เปิดการค้นหา
263
- event.preventDefault();
264
- this.option.openSearch();
265
- break;
266
-
267
- case "F4":
268
- // เปิดเมนูการตั้งค่า
269
- event.preventDefault();
270
- this.option.openSettings();
271
- break;
272
-
273
- case "F5":
274
- // รีโหลดหน้าเว็บ (คงเดิม)
275
- window.location.reload();
276
- break;
277
-
278
- case "F6":
279
- // สลับระหว่างโหมดกลางวัน/กลางคืน
280
- document.body.classList.toggle("dark-mode");
281
- break;
282
-
283
- case "F7":
284
- break;
285
-
286
- case "F8":
287
- break;
288
-
289
- case "F9":
290
- break;
291
-
292
- case "F10":
293
- break;
294
-
295
- case "F11":
296
- if (!document.fullscreenElement) {
297
- document.documentElement
298
- .requestFullscreen()
299
- .catch((err) =>
300
- console.error("Error attempting to enable fullscreen:", err)
301
- );
302
- } else {
303
- document
304
- .exitFullscreen()
305
- .catch((err) =>
306
- console.error("Error attempting to exit fullscreen:", err)
307
- );
308
- }
309
- break;
310
-
311
- case "F12":
312
- break;
313
-
314
- case "HOME":
315
- this.currentInput.setSelectionRange(Math.max(0, 0), Math.max(0, 0));
316
- break;
317
-
318
- case "END":
319
- const length = this.currentInput.value.length;
320
- this.currentInput.setSelectionRange(length, length);
321
- break;
322
-
323
- case "Backspace":
324
- case "backspace":
325
- if (start === end && start > 0) {
326
- this.currentInput.value = value.slice(0, start - 1) + value.slice(end);
327
- this.currentInput.selectionStart = this.currentInput.selectionEnd = start - 1;
328
- } else {
329
- this.currentInput.value = value.slice(0, start) + value.slice(end);
330
- this.currentInput.selectionStart = this.currentInput.selectionEnd = start;
331
- }
332
- break;
333
-
334
- case "DEL⌦":
335
- if (start === end && start < value.length) {
336
- this.currentInput.value = value.slice(0, start) + value.slice(end + 1);
337
- this.currentInput.selectionStart = this.currentInput.selectionEnd = start;
338
- } else {
339
- this.currentInput.value = value.slice(0, start) + value.slice(end);
340
- this.currentInput.selectionStart = this.currentInput.selectionEnd = start;
341
- }
342
- break;
343
-
344
- case "Space":
345
- await this.insertText(" ");
346
- break;
347
-
348
- case "Tab ↹":
349
- await this.insertText("\t");
350
- break;
351
-
352
- case "Enter":
353
- if (this.currentInput.tagName === "TEXTAREA") {
354
- this.currentInput.value = value.slice(0, start) + "\n" + value.slice(end);
355
- this.currentInput.setSelectionRange(start + 1, start + 1);
356
- } else if (this.currentInput.tagName === "INPUT" || this.currentInput.type === "password" || this.currentInput.type === "text" ) {
357
- if (this.currentInput.form) {
358
- const submitButton = this.currentInput.form.querySelector(
359
- 'input[type="submit"], button[type="submit"], button[type="button"], button[onclick]'
360
- );
361
- if (submitButton) submitButton.click(); else this.currentInput.form.submit();
362
- } else {
363
- this.currentInput.value += "\n";
364
- }
365
- }
366
- break;
367
-
368
- case "Caps 🄰":
369
- this.toggleCapsLock();
370
- break;
371
-
372
- case "Shift ⇧":
373
- this.toggleShift();
374
- break;
375
-
376
- case "←":
377
- this.currentInput.setSelectionRange(
378
- Math.max(0, start - 1),
379
- Math.max(0, start - 1)
380
- );
381
- break;
382
-
383
- case "":
384
- this.currentInput.setSelectionRange(start + 1, start + 1);
385
- break;
386
-
387
- case "↑":
388
- case "↓":
389
- const text = this.currentInput.value;
390
- const lines = text.substring(0, start).split("\n");
391
- const currentLineIndex = lines.length - 1;
392
- const currentLine = lines[currentLineIndex];
393
- const columnIndex = start - text.lastIndexOf("\n", start - 1) - 1;
394
-
395
- if (keyPressed === "↑" && currentLineIndex > 0) {
396
- const prevLineLength = lines[currentLineIndex - 1].length;
397
- const newPos = start - currentLine.length - 1 - Math.min(columnIndex, prevLineLength);
398
- this.currentInput.setSelectionRange(newPos, newPos);
399
- } else if (keyPressed === "↓" && currentLineIndex < lines.length - 1) {
400
- const nextLine = lines[currentLineIndex + 1];
401
- const newPos = start + currentLine.length + 1 + Math.min(columnIndex, nextLine.length);
402
- this.currentInput.setSelectionRange(newPos, newPos);
403
- }
404
- break;
405
-
406
- default:
407
- const textvalue = await convertToCorrectCase(keyPressed);
408
- await this.insertText(textvalue);
409
- }
410
-
411
- if (isShiftActive && !isCapsActive) this.toggleShift();
412
-
413
- this.currentInput.focus();
414
- const event = new Event("input", { bubbles: true });
415
- this.currentInput.dispatchEvent(event);
416
- }
417
-
418
- // [3]
419
- async insertText(text) {
420
- const start = this.currentInput.selectionStart;
421
- const end = this.currentInput.selectionEnd;
422
- const textvalue = text;
423
-
424
- this.currentInput.value = this.currentInput.value.slice(0, start) + textvalue + this.currentInput.value.slice(end);
425
- this.currentInput.selectionStart = this.currentInput.selectionEnd = start + textvalue.length;
426
- }
427
-
428
- // [4]
429
- unscrambleKeys() {
430
- this.keys = this.originalKeys;
431
- this.render();
432
- }
433
-
434
- // [5]
435
- toggleCapsLock() {
436
- this.capsLockActive = !this.capsLockActive;
437
-
438
- document.querySelectorAll('.key[data-key="Caps 🄰"]').forEach((key) => {
439
- key.classList.toggle("active", this.capsLockActive);
440
- key.classList.toggle("bg-gray-400", this.capsLockActive);
441
- });
442
-
443
- document.querySelectorAll(".key").forEach((key) => {
444
- const isLetter = key.dataset.key.length === 1 && /[a-zA-Zก-๙]/.test(key.dataset.key);
445
-
446
- if (isLetter) {
447
- key.textContent = this.capsLockActive
448
- ? key.dataset.key.toUpperCase()
449
- : key.dataset.key.toLowerCase();
450
- }
451
- this.updateKeyContent(key, this.capsLockActive);
452
- });
453
- }
454
- updateKeyContent(key, capsLockActive) {
455
- const currentChar = key.textContent.trim();
456
-
457
- const layouts = {
458
- th: this.ThaiAlphabetShift,
459
- en: this.EngAlphabetShift,
460
- enSc: this.EngAlphabetShift,
461
- full: this.FullAlphabetShift,
462
- };
463
- const layout = layouts[this.currentLayout];
464
-
465
- if (!layout) return;
466
-
467
- if (capsLockActive && layout[currentChar]) {
468
- key.textContent = layout[currentChar];
469
- key.dataset.key = layout[currentChar];
470
- } else if (!capsLockActive && Object.values(layout).includes(currentChar)) {
471
- const originalKey = Object.keys(layout).find((k) => layout[k] === currentChar);
472
- if (originalKey) {
473
- key.textContent = originalKey;
474
- key.dataset.key = originalKey;
475
- }
476
- }
477
- }
478
-
479
- // [6]
480
- toggleShift() {
481
- if (this.capsLockActive) {
482
- return this.toggleCapsLock();
483
- }
484
-
485
- this.shiftActive = !this.shiftActive;
486
- document.querySelectorAll('.key[data-key="Shift ⇧"]').forEach((key) => {
487
- key.classList.toggle("active", this.shiftActive);
488
- key.classList.toggle("bg-gray-400", this.shiftActive);
489
- });
490
-
491
- document.querySelectorAll(".key").forEach((key) => {
492
- const isLetter = key.dataset.key.length === 1 && /[a-zA-Zก-๙]/.test(key.dataset.key);
493
-
494
- if (isLetter) {
495
- key.textContent = this.shiftActive
496
- ? key.dataset.key.toUpperCase()
497
- : key.dataset.key.toLowerCase();
498
- }
499
- this.updateKeyContent(key, this.shiftActive);
500
- });
501
- }
502
- updateKeyContent(key, shiftActive) {
503
- const currentChar = key.textContent.trim();
504
-
505
- const layouts = {
506
- th: this.ThaiAlphabetShift,
507
- en: this.EngAlphabetShift,
508
- enSc: this.EngAlphabetShift,
509
- full: this.FullAlphabetShift,
510
- };
511
- const layout = layouts[this.currentLayout];
512
-
513
- if (!layout) return;
514
-
515
- if (shiftActive && layout[currentChar]) {
516
- key.textContent = layout[currentChar];
517
- key.dataset.key = layout[currentChar];
518
- } else if (!shiftActive && Object.values(layout).includes(currentChar)) {
519
- const originalKey = Object.keys(layout).find((k) => layout[k] === currentChar);
520
- if (originalKey) {
521
- key.textContent = originalKey;
522
- key.dataset.key = originalKey;
523
- }
524
- }
525
- }
526
-
527
- // [7]
528
- EngAlphabetShift = { "`": "~", 1: "!", 2: "@", 3: "#", 4: "$", 5: "%", 6: "^", 7: "&", 8: "*", 9: "(", 0: ")", "-": "_", "=": "+", "[": "{", "]": "}", "\\": "|", ";": ":", "'": '"', ",": "<", ".": ">", "/": "?", };
529
- ThaiAlphabetShift = { _: "%", ๅ: "+", "/": "๑", "-": "๒", ภ: "๓", ถ: "๔", "ุ": "ู", "ึ": "฿", ค: "๕", ต: "๖", จ: "๗", ข: "๘", ช: "๙", ๆ: "๐", ไ: '"', ำ: "ฎ", พ: "ฑ", ะ: "ธ", "ั": "ํ", "ี": "๋", ร: "ณ", น: "ฯ", ย: "ญ", บ: "ฐ", ล: ",", ฃ: "ฅ", ฟ: "ฤ", ห: "ฆ", ก: "ฏ", ด: "โ", เ: "ฌ", "้": "็", "่": "๋", า: "ษ", ส: "ศ", ว: "ซ", ง: ".", ผ: "(", ป: ")", แ: "ฉ", อ: "ฮ", "ิ": "ฺ", "ื": "์", ท: "?", ม: "ฒ", ใ: "ฬ", ฝ: "ฦ"};
530
- FullAlphabetShift = { "[": "{", "]": "}", "\\": "|", ";": ":", "'": '"', ",": "<", ".": ">", "/": "?", };
531
-
532
- // [8]
533
- toggle() {
534
- this.isVisible = !this.isVisible;
535
- this.render();
536
- }
537
-
538
- // [9]
539
- changeLayout(layout) {
540
- this.currentLayout = layout;
541
- this.render();
542
- }
543
-
544
- // [10]
545
- shuffleArray(array) {
546
- for (let i = array.length - 1; i > 0; i--) {
547
- const j = Math.floor(Math.random() * (i + 1));
548
- [array[i], array[j]] = [array[j], array[i]];
549
- }
550
- }
551
-
552
- // [11]
553
- scrambleKeyboard() {
554
- const keys = document.querySelectorAll(
555
- ":not([data-key='std']):not([data-key='scr']).key:not([data-key=backspace]"
556
- );
557
- const numbers = "1234567890-=+%/*.()".split("");
558
- this.shuffleArray(numbers);
559
- keys.forEach((key, index) => {
560
- key.textContent = numbers[index];
561
- key.dataset.key = numbers[index];
562
- });
563
- }
564
-
565
- // [12]
566
- scrambleEnglishKeys() {
567
- const keys = document.querySelectorAll(
568
- ".key:not([data-key='scrambled']):not([data-key='std']):not([data-key='scr']):not([data-key='Space']):not([data-key='Backspace']):not([data-key='Caps 🄰']):not([data-key='Shift ⇧']):not([data-key='Enter']):not([data-key='Tab ↹'])"
569
- );
570
- const englishAlphabet = "abcdefghijklmnopqrstuvwxyz/.,';\\][`1234567890-=".split("");
571
- this.shuffleArray(englishAlphabet);
572
- keys.forEach((key, index) => {
573
- key.textContent = englishAlphabet[index];
574
- key.dataset.key = englishAlphabet[index];
575
- });
576
- }
577
-
578
- // [13]
579
- scrambleThaiKeys() {
580
- const keys = document.querySelectorAll(
581
- ".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='Space']):not([data-key='Tab ↹'])"
582
- );
583
- const ThaiAlphabet = '_ๅ/-ภถุึคตจขชๆไำพะัีรนยบลฃฟหกดเ้่าสวงผปแอิืทมใฝ%+๑๒๓๔ู฿๕๖๗๘๙๐"ฎฑธํ๊ณฯญฐ,ฅฤฆฏโฌ็๋ษศซ.ฦฬฒ?์ฺฮฉ)('.split("");
584
- this.shuffleArray(ThaiAlphabet);
585
- keys.forEach((key, index) => {
586
- key.textContent = ThaiAlphabet[index];
587
- key.dataset.key = ThaiAlphabet[index];
588
- });
589
- }
590
-
591
- // []
592
- scrambleSymbols() {
593
- const keys = document.querySelectorAll(
594
- ":not([data-key='std']):not([data-key='scr']).key:not([data-key=backspace]"
595
- );
596
- const numbers = "@#$%^&*()_+~`{}|\\:'<>?/[]±§¶!€£¥¢©®™℅‰†".split("");
597
- this.shuffleArray(numbers);
598
- keys.forEach((key, index) => {
599
- key.textContent = numbers[index];
600
- key.dataset.key = numbers[index];
601
- });
602
- }
603
-
604
- // [14] จัดการการลากคีย์บอร์ด
605
- startDrag(event) {
606
- this.isDragging = true;
607
- this.offsetX = event.clientX - document.getElementById("keyboard").offsetLeft;
608
- this.offsetY = event.clientY - document.getElementById("keyboard").offsetTop;
609
-
610
- document.addEventListener("mousemove", this.drag.bind(this));
611
- document.addEventListener("mouseup", () => {
612
- this.isDragging = false;
613
- document.removeEventListener("mousemove", this.drag.bind(this));
614
- });
615
- }
616
-
617
- // [15]
618
- drag(event) {
619
- if (this.isDragging) {
620
- const keyboard = document.getElementById("keyboard");
621
- keyboard.style.left = `${event.clientX - this.offsetX}px`;
622
- keyboard.style.top = `${event.clientY - this.offsetY}px`;
623
- }
624
- }
1
+ import { layouts } from "./layouts.js";
2
+
3
+ export class VirtualKeyboard {
4
+ constructor() {
5
+ this.currentLayout = "full";
6
+ this.isVisible = false;
7
+ this.container = document.getElementById("keyboard-container");
8
+ this.currentInput = null;
9
+ this.layouts = layouts;
10
+ this.isDragging = false;
11
+ this.offsetX = 0;
12
+ this.offsetY = 0;
13
+ this.shiftActive = false;
14
+ this.capsLockActive = false;
15
+ this.isScrambled = false;
16
+
17
+ // Maintain plaintext buffers per input (not stored in DOM). DOM will store only encrypted text.
18
+ this.inputPlaintextBuffers = new WeakMap();
19
+
20
+ // Security state
21
+ this.security = {
22
+ detectorStatus: "idle",
23
+ suspiciousEvents: [],
24
+ observer: null,
25
+ addSuspicious(event) {
26
+ try {
27
+ this.suspiciousEvents.push({
28
+ time: new Date().toISOString(),
29
+ ...event,
30
+ });
31
+ console.warn("[VK Security] Suspicious activity detected:", event);
32
+ } catch (_) {}
33
+ },
34
+ };
35
+
36
+ this.initialize();
37
+ }
38
+
39
+ async initialize() {
40
+ try {
41
+ this.applyCSPReportOnly();
42
+ this.startKeyloggerDetector();
43
+ this.render();
44
+ this.initializeInputListeners();
45
+ console.log("VirtualKeyboard initialized successfully.");
46
+ } catch (error) {
47
+ console.error("Error initializing VirtualKeyboard:", error);
48
+ }
49
+ }
50
+
51
+ // Inject CSP Report-Only meta for runtime visibility into potential violations
52
+ applyCSPReportOnly() {
53
+ // Disabled: Report-Only CSP must be set via HTTP headers. See server middleware in app.js.
54
+ return;
55
+ }
56
+
57
+ // Start MutationObserver and limited listener-hook to detect potential keylogger injections
58
+ startKeyloggerDetector() {
59
+ try {
60
+ const allowlistedScriptHosts = new Set([
61
+ window.location.host,
62
+ "cdnjs.cloudflare.com",
63
+ ]);
64
+
65
+ const isSuspiciousScript = (node) => {
66
+ try {
67
+ if (!(node instanceof HTMLScriptElement)) return false;
68
+ const src = node.getAttribute("src") || "";
69
+ if (!src) {
70
+ const code = (node.textContent || "").toLowerCase();
71
+ return /addEventListener\(['\"]key|document\.onkey|keylogger|send\(|fetch\(|xmlhttprequest/i.test(code);
72
+ }
73
+ const url = new URL(src, window.location.href);
74
+ return !Array.from(allowlistedScriptHosts).some((h) => url.host.endsWith(h));
75
+ } catch (_) {
76
+ return true;
77
+ }
78
+ };
79
+
80
+ const isSuspiciousElement = (node) => {
81
+ try {
82
+ if (!(node instanceof Element)) return false;
83
+ const hasKeyHandlers = node.hasAttribute("onkeydown") || node.hasAttribute("onkeypress") || node.hasAttribute("onkeyup");
84
+ const hiddenFrame = node.tagName === "IFRAME" && (node.getAttribute("sandbox") === null || node.getAttribute("srcdoc") !== null);
85
+ return hasKeyHandlers || hiddenFrame;
86
+ } catch (_) {
87
+ return false;
88
+ }
89
+ };
90
+
91
+ const quarantineScript = (script) => {
92
+ try {
93
+ script.type = "text/plain";
94
+ script.setAttribute("data-quarantined", "true");
95
+ } catch (_) {}
96
+ };
97
+
98
+ const observer = new MutationObserver((mutations) => {
99
+ for (const m of mutations) {
100
+ if (m.type === "childList") {
101
+ m.addedNodes.forEach((node) => {
102
+ if (isSuspiciousScript(node)) {
103
+ this.security.addSuspicious({ type: "script", reason: "suspicious source or inline pattern", node });
104
+ quarantineScript(node);
105
+ } else if (isSuspiciousElement(node)) {
106
+ this.security.addSuspicious({ type: "element", reason: "inline key handlers or risky frame", node });
107
+ }
108
+ });
109
+ } else if (m.type === "attributes" && isSuspiciousElement(m.target)) {
110
+ this.security.addSuspicious({ type: "attr", reason: `attribute ${m.attributeName}`, node: m.target });
111
+ }
112
+ }
113
+ });
114
+
115
+ observer.observe(document.documentElement, {
116
+ childList: true,
117
+ subtree: true,
118
+ attributes: true,
119
+ attributeFilter: ["onkeydown", "onkeypress", "onkeyup", "src", "type", "sandbox", "srcdoc"],
120
+ });
121
+
122
+ // Hook addEventListener to monitor keyboard listeners registration
123
+ const OriginalAddEventListener = EventTarget.prototype.addEventListener;
124
+ const selfRef = this;
125
+ EventTarget.prototype.addEventListener = function(type, listener, options) {
126
+ try {
127
+ if (type && /^(key(?:down|up|press))$/i.test(type)) {
128
+ const info = {
129
+ type: "event-listener",
130
+ reason: `keyboard listener registered: ${type}`,
131
+ target: this,
132
+ };
133
+ selfRef.security.addSuspicious(info);
134
+ }
135
+ } catch (_) {}
136
+ return OriginalAddEventListener.call(this, type, listener, options);
137
+ };
138
+
139
+ this.security.observer = observer;
140
+ this.security.detectorStatus = "running";
141
+
142
+ // Expose minimal security API
143
+ window.VKSecurity = {
144
+ getStatus: () => this.security.detectorStatus,
145
+ getEvents: () => [...this.security.suspiciousEvents],
146
+ stop: () => {
147
+ try { observer.disconnect(); } catch (_) {}
148
+ this.security.detectorStatus = "stopped";
149
+ },
150
+ };
151
+ console.info("[VK Security] Keylogger detector started");
152
+ } catch (err) {
153
+ this.security.detectorStatus = "error";
154
+ console.warn("[VK Security] Detector error:", err);
155
+ }
156
+ }
157
+
158
+ getLayoutName(layout) {
159
+ switch (layout) {
160
+ case "full":
161
+ return "Full Keyboard";
162
+ case "en":
163
+ return "English Keyboard";
164
+ case "th":
165
+ return "Thai keyboard";
166
+ case "numpad":
167
+ return "Numpad Keyboard";
168
+ case "symbols":
169
+ return "Symbols Keyboard";
170
+ default:
171
+ return "Unknown Layout";
172
+ }
173
+ }
174
+
175
+ initializeInputListeners() {
176
+ document.addEventListener("click", (e) => {
177
+ const target = e.target;
178
+ if (target.tagName === "INPUT" || target.tagName === "TEXTAREA") {
179
+ this.setCurrentInput(target);
180
+ }
181
+ });
182
+
183
+ document.addEventListener(
184
+ "focus",
185
+ (e) => {
186
+ const target = e.target;
187
+ if (target.tagName === "INPUT" || target.tagName === "TEXTAREA") {
188
+ this.setCurrentInput(target);
189
+ }
190
+ },
191
+ true
192
+ );
193
+
194
+ const toggle = document.getElementById("toggle");
195
+ if (toggle) {
196
+ toggle.addEventListener("click", this.toggle.bind(this));
197
+ }
198
+ }
199
+
200
+ setCurrentInput(inputElement) {
201
+ if (this.currentInput) {
202
+ this.currentInput.classList.remove("keyboard-active");
203
+ }
204
+
205
+ this.currentInput = inputElement;
206
+ this.currentInput.classList.add("keyboard-active");
207
+
208
+ // Ensure buffer initialized for this input
209
+ if (!this.inputPlaintextBuffers.has(this.currentInput)) {
210
+ const ds = this.currentInput.dataset ? this.currentInput.dataset.encrypted : undefined;
211
+ let initial = "";
212
+ try {
213
+ if (ds && window.VKCrypto && typeof window.VKCrypto.decrypt === "function") {
214
+ initial = window.VKCrypto.decrypt(ds) || "";
215
+ } else {
216
+ initial = this.currentInput.value || "";
217
+ }
218
+ } catch (_) {
219
+ initial = this.currentInput.value || "";
220
+ }
221
+ this.inputPlaintextBuffers.set(this.currentInput, initial);
222
+ this.updateDomEncrypted(this.currentInput, initial);
223
+ }
224
+ // Sync visible with buffer (masking for password)
225
+ this.updateDisplayedValue();
226
+ }
227
+
228
+ // [1]
229
+ render() {
230
+ const keyboard = document.createElement("div");
231
+ keyboard.className = `virtual-keyboard ${this.currentLayout}`;
232
+ keyboard.style.display = this.isVisible ? "block" : "none";
233
+ keyboard.id = "keyboard";
234
+
235
+ const controlsContainer = document.createElement("div");
236
+ controlsContainer.className = "controls";
237
+ controlsContainer.style.display = "flex";
238
+ controlsContainer.style.justifyContent = "center";
239
+ controlsContainer.style.alignItems = "center";
240
+ controlsContainer.style.marginBottom = "10px";
241
+
242
+ const layoutSelector = document.createElement("select");
243
+ layoutSelector.id = "layout-selector";
244
+ layoutSelector.onchange = (e) => this.changeLayout(e.target.value);
245
+
246
+ const layouts = ["full", "en", "th", "numpad", "symbols"];
247
+ layouts.forEach((layout) => {
248
+ const option = document.createElement("option");
249
+ option.value = layout;
250
+ option.innerText = this.getLayoutName(layout);
251
+ layoutSelector.appendChild(option);
252
+ });
253
+ layoutSelector.value = this.currentLayout;
254
+ controlsContainer.appendChild(layoutSelector);
255
+
256
+ keyboard.appendChild(controlsContainer);
257
+
258
+ const layout = this.layouts[this.currentLayout];
259
+
260
+ layout.forEach((row) => {
261
+ const rowElement = document.createElement("div");
262
+ rowElement.className = "keyboard-row";
263
+
264
+ row.forEach((key, index) => {
265
+ const keyElement = document.createElement("button");
266
+ keyElement.className = "keyboard-key key";
267
+ keyElement.textContent = key;
268
+ keyElement.type = "button";
269
+
270
+ keyElement.dataset.key = key;
271
+
272
+ if (index >= row.length - 4) {
273
+ keyElement.classList.add("concat-keys");
274
+ }
275
+
276
+ if (key === "Space") {
277
+ keyElement.className += " space";
278
+ }
279
+
280
+ if (key === "backspace" || key === "Backspace") {
281
+ keyElement.className += " backspacew";
282
+ keyElement.innerHTML = '<i class="fa fa-backspace"></i>';
283
+ }
284
+
285
+ keyElement.onclick = (e) => {
286
+ e.preventDefault();
287
+ const keyPressed = keyElement.dataset.key || keyElement.textContent;
288
+ if (keyPressed) {
289
+ this.handleKeyPress(keyPressed);
290
+ } else {
291
+ console.error("The key element does not have a valid key value.");
292
+ }
293
+ };
294
+
295
+ rowElement.appendChild(keyElement);
296
+ });
297
+
298
+ keyboard.appendChild(rowElement);
299
+ });
300
+
301
+ this.container.innerHTML = "";
302
+ this.container.appendChild(keyboard);
303
+
304
+ keyboard.addEventListener("mousedown", (event) => this.startDrag(event));
305
+ }
306
+
307
+ // [2]
308
+ async handleKeyPress(keyPressed) {
309
+ if (!this.currentInput) return;
310
+ const start = this.currentInput.selectionStart;
311
+ const end = this.currentInput.selectionEnd;
312
+ const buffer = this.getCurrentBuffer();
313
+
314
+ const isCapsActive = this.capsLockActive;
315
+ const isShiftActive = this.shiftActive;
316
+
317
+ const convertToCorrectCase = (char) => {
318
+ if (isCapsActive || isShiftActive) {
319
+ return char.toUpperCase();
320
+ }
321
+ return char.toLowerCase();
322
+ };
323
+
324
+ if (!keyPressed) return console.error("Invalid key pressed.");
325
+
326
+ if (keyPressed === "scr" || keyPressed === "scrambled") {
327
+ if (this.currentLayout === "en") {
328
+ if (this.isScrambled) {
329
+ this.unscrambleKeys();
330
+ } else {
331
+ this.scrambleEnglishKeys();
332
+ }
333
+ this.isScrambled = !this.isScrambled;
334
+ document
335
+ .querySelectorAll('.key[data-key="scrambled"]')
336
+ .forEach((key) => {
337
+ key.classList.toggle("active", this.isScrambled);
338
+ });
339
+ }
340
+
341
+ if (this.currentLayout === "th") {
342
+ if (this.isScrambled) {
343
+ this.unscrambleKeys();
344
+ } else {
345
+ this.scrambleThaiKeys();
346
+ }
347
+ this.isScrambled = !this.isScrambled;
348
+ document
349
+ .querySelectorAll('.key[data-key="scrambled"]')
350
+ .forEach((key) => {
351
+ key.classList.toggle("active", this.isScrambled);
352
+ });
353
+ }
354
+
355
+ if (this.currentLayout === "numpad") {
356
+ if (this.isScrambled) {
357
+ this.unscrambleKeys();
358
+ } else {
359
+ this.scrambleKeyboard();
360
+ }
361
+ this.isScrambled = !this.isScrambled;
362
+ document.querySelectorAll('.key[data-key="scr"]').forEach((key) => {
363
+ key.classList.toggle("active", this.isScrambled);
364
+ });
365
+ }
366
+
367
+ if (this.currentLayout === "symbols") {
368
+ if (this.isScrambled) {
369
+ this.unscrambleKeys();
370
+ } else {
371
+ this.scrambleSymbols();
372
+ }
373
+ this.isScrambled = !this.isScrambled;
374
+ document.querySelectorAll('.key[data-key="scr"]').forEach((key) => {
375
+ key.classList.toggle("active", this.isScrambled);
376
+ });
377
+ }
378
+ return;
379
+ }
380
+
381
+ switch (keyPressed) {
382
+ case "Esc":
383
+ const modals = document.querySelectorAll(".modal");
384
+ if (modals.length === 0) {
385
+ document.exitFullscreen();
386
+ console.warn("No modals found to close.");
387
+ }
388
+ modals.forEach((modal) => {
389
+ modal.classList.add("hidden");
390
+ });
391
+ break;
392
+
393
+ case "F1":
394
+ break;
395
+
396
+ case "F2":
397
+ // เปิดโหมดแก้ไขสำหรับ element ที่เลือก
398
+ const activeElement = document.activeElement;
399
+ if (activeElement && activeElement.contentEditable !== undefined) {
400
+ activeElement.contentEditable = true;
401
+ activeElement.focus();
402
+ console.log(activeElement.contentEditable);
403
+ } else {
404
+ console.warn("No editable element found.");
405
+ }
406
+ break;
407
+
408
+ case "F3":
409
+ // เปิดการค้นหา
410
+ event.preventDefault();
411
+ this.option.openSearch();
412
+ break;
413
+
414
+ case "F4":
415
+ // เปิดเมนูการตั้งค่า
416
+ event.preventDefault();
417
+ this.option.openSettings();
418
+ break;
419
+
420
+ case "F5":
421
+ // รีโหลดหน้าเว็บ (คงเดิม)
422
+ window.location.reload();
423
+ break;
424
+
425
+ case "F6":
426
+ // สลับระหว่างโหมดกลางวัน/กลางคืน
427
+ document.body.classList.toggle("dark-mode");
428
+ break;
429
+
430
+ case "F7":
431
+ break;
432
+
433
+ case "F8":
434
+ break;
435
+
436
+ case "F9":
437
+ break;
438
+
439
+ case "F10":
440
+ break;
441
+
442
+ case "F11":
443
+ if (!document.fullscreenElement) {
444
+ document.documentElement
445
+ .requestFullscreen()
446
+ .catch((err) =>
447
+ console.error("Error attempting to enable fullscreen:", err)
448
+ );
449
+ } else {
450
+ document
451
+ .exitFullscreen()
452
+ .catch((err) =>
453
+ console.error("Error attempting to exit fullscreen:", err)
454
+ );
455
+ }
456
+ break;
457
+
458
+ case "F12":
459
+ break;
460
+
461
+ case "HOME":
462
+ this.currentInput.setSelectionRange(Math.max(0, 0), Math.max(0, 0));
463
+ break;
464
+
465
+ case "END":
466
+ const length = this.currentInput.value.length;
467
+ this.currentInput.setSelectionRange(length, length);
468
+ break;
469
+
470
+ case "Backspace":
471
+ case "backspace":
472
+ if (start === end && start > 0) {
473
+ const newBuffer = buffer.slice(0, start - 1) + buffer.slice(end);
474
+ this.setCurrentBuffer(newBuffer);
475
+ this.currentInput.selectionStart = this.currentInput.selectionEnd = start - 1;
476
+ } else {
477
+ const newBuffer = buffer.slice(0, start) + buffer.slice(end);
478
+ this.setCurrentBuffer(newBuffer);
479
+ this.currentInput.selectionStart = this.currentInput.selectionEnd = start;
480
+ }
481
+ this.updateDisplayedValue();
482
+ break;
483
+
484
+ case "DEL⌦":
485
+ if (start === end && start < buffer.length) {
486
+ const newBuffer = buffer.slice(0, start) + buffer.slice(end + 1);
487
+ this.setCurrentBuffer(newBuffer);
488
+ this.currentInput.selectionStart = this.currentInput.selectionEnd = start;
489
+ } else {
490
+ const newBuffer = buffer.slice(0, start) + buffer.slice(end);
491
+ this.setCurrentBuffer(newBuffer);
492
+ this.currentInput.selectionStart = this.currentInput.selectionEnd = start;
493
+ }
494
+ this.updateDisplayedValue();
495
+ break;
496
+
497
+ case "Space":
498
+ await this.insertText(" ");
499
+ break;
500
+
501
+ case "Tab ↹":
502
+ await this.insertText("\t");
503
+ break;
504
+
505
+ case "Enter":
506
+ if (this.currentInput.tagName === "TEXTAREA") {
507
+ const newBuffer = buffer.slice(0, start) + "\n" + buffer.slice(end);
508
+ this.setCurrentBuffer(newBuffer);
509
+ this.updateDisplayedValue();
510
+ this.currentInput.setSelectionRange(start + 1, start + 1);
511
+ } else if (this.currentInput.tagName === "INPUT" || this.currentInput.type === "password" || this.currentInput.type === "text" ) {
512
+ if (this.currentInput.form) {
513
+ const submitButton = this.currentInput.form.querySelector(
514
+ 'input[type="submit"], button[type="submit"], button[type="button"], button[onclick]'
515
+ );
516
+ if (submitButton) submitButton.click(); else this.currentInput.form.submit();
517
+ } else {
518
+ const newBuffer = buffer + "\n";
519
+ this.setCurrentBuffer(newBuffer);
520
+ this.updateDisplayedValue();
521
+ }
522
+ }
523
+ break;
524
+
525
+ case "Caps 🄰":
526
+ this.toggleCapsLock();
527
+ break;
528
+
529
+ case "Shift ":
530
+ this.toggleShift();
531
+ break;
532
+
533
+ case "←":
534
+ this.currentInput.setSelectionRange(
535
+ Math.max(0, start - 1),
536
+ Math.max(0, start - 1)
537
+ );
538
+ break;
539
+
540
+ case "→":
541
+ this.currentInput.setSelectionRange(start + 1, start + 1);
542
+ break;
543
+
544
+ case "↑":
545
+ case "↓":
546
+ const text = this.currentInput.value;
547
+ const lines = text.substring(0, start).split("\n");
548
+ const currentLineIndex = lines.length - 1;
549
+ const currentLine = lines[currentLineIndex];
550
+ const columnIndex = start - text.lastIndexOf("\n", start - 1) - 1;
551
+
552
+ if (keyPressed === "↑" && currentLineIndex > 0) {
553
+ const prevLineLength = lines[currentLineIndex - 1].length;
554
+ const newPos = start - currentLine.length - 1 - Math.min(columnIndex, prevLineLength);
555
+ this.currentInput.setSelectionRange(newPos, newPos);
556
+ } else if (keyPressed === "↓" && currentLineIndex < lines.length - 1) {
557
+ const nextLine = lines[currentLineIndex + 1];
558
+ const newPos = start + currentLine.length + 1 + Math.min(columnIndex, nextLine.length);
559
+ this.currentInput.setSelectionRange(newPos, newPos);
560
+ }
561
+ break;
562
+
563
+ default:
564
+ const textvalue = await convertToCorrectCase(keyPressed);
565
+ await this.insertText(textvalue);
566
+ }
567
+
568
+ if (isShiftActive && !isCapsActive) this.toggleShift();
569
+
570
+ this.currentInput.focus();
571
+ const event = new Event("input", { bubbles: true });
572
+ this.currentInput.dispatchEvent(event);
573
+ }
574
+
575
+ // [3]
576
+ async insertText(text) {
577
+ const start = this.currentInput.selectionStart;
578
+ const end = this.currentInput.selectionEnd;
579
+ const textvalue = text;
580
+
581
+ const buffer = this.getCurrentBuffer();
582
+ const newBuffer = buffer.slice(0, start) + textvalue + buffer.slice(end);
583
+ this.setCurrentBuffer(newBuffer);
584
+ this.updateDisplayedValue();
585
+ this.currentInput.selectionStart = this.currentInput.selectionEnd = start + textvalue.length;
586
+ }
587
+
588
+ // Helper: get current plaintext buffer for active input
589
+ getCurrentBuffer() {
590
+ if (!this.currentInput) return "";
591
+ return this.inputPlaintextBuffers.get(this.currentInput) || "";
592
+ }
593
+
594
+ // Helper: set buffer and update encrypted dataset
595
+ setCurrentBuffer(newBuffer) {
596
+ if (!this.currentInput) return;
597
+ this.inputPlaintextBuffers.set(this.currentInput, newBuffer);
598
+ this.updateDomEncrypted(this.currentInput, newBuffer);
599
+ }
600
+
601
+ // Helper: reflect masked value in the visible input/textarea
602
+ updateDisplayedValue() {
603
+ if (!this.currentInput) return;
604
+ const buffer = this.getCurrentBuffer();
605
+ const isPassword = this.currentInput.type === "password";
606
+ const maskChar = "•";
607
+ this.currentInput.value = isPassword ? maskChar.repeat(buffer.length) : buffer;
608
+ }
609
+
610
+ // Helper: encrypt and store ciphertext in DOM attribute
611
+ updateDomEncrypted(input, plaintext) {
612
+ try {
613
+ const crypto = window.VKCrypto;
614
+
615
+ if (crypto && typeof crypto.encrypt === "function") {
616
+ const cipher = plaintext.length ? crypto.encrypt(plaintext) : "";
617
+ input.dataset.encrypted = cipher;
618
+ } else {
619
+ // No encryption function available — don't store anything
620
+ console.warn("Encryption unavailable. Not storing plaintext.");
621
+ input.dataset.encrypted = "";
622
+ }
623
+
624
+ } catch (err) {
625
+ console.error("Failed to encrypt input:", err);
626
+ input.dataset.encrypted = "";
627
+ }
628
+ }
629
+
630
+
631
+ // [4]
632
+ unscrambleKeys() {
633
+ this.keys = this.originalKeys;
634
+ this.render();
635
+ }
636
+
637
+ // [5]
638
+ toggleCapsLock() {
639
+ this.capsLockActive = !this.capsLockActive;
640
+
641
+ document.querySelectorAll('.key[data-key="Caps 🄰"]').forEach((key) => {
642
+ key.classList.toggle("active", this.capsLockActive);
643
+ key.classList.toggle("bg-gray-400", this.capsLockActive);
644
+ });
645
+
646
+ document.querySelectorAll(".key").forEach((key) => {
647
+ const isLetter = key.dataset.key.length === 1 && /[a-zA-Zก-๙]/.test(key.dataset.key);
648
+
649
+ if (isLetter) {
650
+ key.textContent = this.capsLockActive
651
+ ? key.dataset.key.toUpperCase()
652
+ : key.dataset.key.toLowerCase();
653
+ }
654
+ this.updateKeyContent(key, this.capsLockActive);
655
+ });
656
+ }
657
+ updateKeyContent(key, capsLockActive) {
658
+ const currentChar = key.textContent.trim();
659
+
660
+ const layouts = {
661
+ th: this.ThaiAlphabetShift,
662
+ en: this.EngAlphabetShift,
663
+ enSc: this.EngAlphabetShift,
664
+ full: this.FullAlphabetShift,
665
+ };
666
+ const layout = layouts[this.currentLayout];
667
+
668
+ if (!layout) return;
669
+
670
+ if (capsLockActive && layout[currentChar]) {
671
+ key.textContent = layout[currentChar];
672
+ key.dataset.key = layout[currentChar];
673
+ } else if (!capsLockActive && Object.values(layout).includes(currentChar)) {
674
+ const originalKey = Object.keys(layout).find((k) => layout[k] === currentChar);
675
+ if (originalKey) {
676
+ key.textContent = originalKey;
677
+ key.dataset.key = originalKey;
678
+ }
679
+ }
680
+ }
681
+
682
+ // [6]
683
+ toggleShift() {
684
+ if (this.capsLockActive) {
685
+ return this.toggleCapsLock();
686
+ }
687
+
688
+ this.shiftActive = !this.shiftActive;
689
+ document.querySelectorAll('.key[data-key="Shift ⇧"]').forEach((key) => {
690
+ key.classList.toggle("active", this.shiftActive);
691
+ key.classList.toggle("bg-gray-400", this.shiftActive);
692
+ });
693
+
694
+ document.querySelectorAll(".key").forEach((key) => {
695
+ const isLetter = key.dataset.key.length === 1 && /[a-zA-Zก-๙]/.test(key.dataset.key);
696
+
697
+ if (isLetter) {
698
+ key.textContent = this.shiftActive
699
+ ? key.dataset.key.toUpperCase()
700
+ : key.dataset.key.toLowerCase();
701
+ }
702
+ this.updateKeyContent(key, this.shiftActive);
703
+ });
704
+ }
705
+ updateKeyContent(key, shiftActive) {
706
+ const currentChar = key.textContent.trim();
707
+
708
+ const layouts = {
709
+ th: this.ThaiAlphabetShift,
710
+ en: this.EngAlphabetShift,
711
+ enSc: this.EngAlphabetShift,
712
+ full: this.FullAlphabetShift,
713
+ };
714
+ const layout = layouts[this.currentLayout];
715
+
716
+ if (!layout) return;
717
+
718
+ if (shiftActive && layout[currentChar]) {
719
+ key.textContent = layout[currentChar];
720
+ key.dataset.key = layout[currentChar];
721
+ } else if (!shiftActive && Object.values(layout).includes(currentChar)) {
722
+ const originalKey = Object.keys(layout).find((k) => layout[k] === currentChar);
723
+ if (originalKey) {
724
+ key.textContent = originalKey;
725
+ key.dataset.key = originalKey;
726
+ }
727
+ }
728
+ }
729
+
730
+ // [7]
731
+ EngAlphabetShift = { "`": "~", 1: "!", 2: "@", 3: "#", 4: "$", 5: "%", 6: "^", 7: "&", 8: "*", 9: "(", 0: ")", "-": "_", "=": "+", "[": "{", "]": "}", "\\": "|", ";": ":", "'": '"', ",": "<", ".": ">", "/": "?", };
732
+ ThaiAlphabetShift = { _: "%", ๅ: "+", "/": "๑", "-": "๒", ภ: "๓", ถ: "๔", "ุ": "ู", "ึ": "฿", ค: "๕", ต: "๖", จ: "๗", ข: "๘", ช: "๙", ๆ: "๐", ไ: '"', ำ: "ฎ", พ: "ฑ", ะ: "ธ", "ั": "ํ", "ี": "๋", ร: "ณ", น: "ฯ", ย: "ญ", บ: "ฐ", ล: ",", ฃ: "ฅ", ฟ: "ฤ", ห: "ฆ", ก: "ฏ", ด: "โ", เ: "ฌ", "้": "็", "่": "๋", า: "ษ", ส: "ศ", ว: "ซ", ง: ".", ผ: "(", ป: ")", แ: "ฉ", อ: "ฮ", "ิ": "ฺ", "ื": "์", ท: "?", ม: "ฒ", ใ: "ฬ", ฝ: "ฦ"};
733
+ FullAlphabetShift = { "[": "{", "]": "}", "\\": "|", ";": ":", "'": '"', ",": "<", ".": ">", "/": "?", };
734
+
735
+ // [8]
736
+ toggle() {
737
+ this.isVisible = !this.isVisible;
738
+ this.render();
739
+ }
740
+
741
+ // [9]
742
+ changeLayout(layout) {
743
+ this.currentLayout = layout;
744
+ this.render();
745
+ }
746
+
747
+ // [10]
748
+ shuffleArray(array) {
749
+ for (let i = array.length - 1; i > 0; i--) {
750
+ const j = Math.floor(Math.random() * (i + 1));
751
+ [array[i], array[j]] = [array[j], array[i]];
752
+ }
753
+ }
754
+
755
+ // [11]
756
+ scrambleKeyboard() {
757
+ const keys = document.querySelectorAll(
758
+ ":not([data-key='std']):not([data-key='scr']).key:not([data-key=backspace]"
759
+ );
760
+ const numbers = "1234567890-=+%/*.()".split("");
761
+ this.shuffleArray(numbers);
762
+ keys.forEach((key, index) => {
763
+ key.textContent = numbers[index];
764
+ key.dataset.key = numbers[index];
765
+ });
766
+ }
767
+
768
+ // [12]
769
+ scrambleEnglishKeys() {
770
+ const keys = document.querySelectorAll(
771
+ ".key:not([data-key='scrambled']):not([data-key='std']):not([data-key='scr']):not([data-key='Space']):not([data-key='Backspace']):not([data-key='Caps 🄰']):not([data-key='Shift ⇧']):not([data-key='Enter']):not([data-key='Tab ↹'])"
772
+ );
773
+ const englishAlphabet = "abcdefghijklmnopqrstuvwxyz/.,';\\][`1234567890-=".split("");
774
+ this.shuffleArray(englishAlphabet);
775
+ keys.forEach((key, index) => {
776
+ key.textContent = englishAlphabet[index];
777
+ key.dataset.key = englishAlphabet[index];
778
+ });
779
+ }
780
+
781
+ // [13]
782
+ scrambleThaiKeys() {
783
+ const keys = document.querySelectorAll(
784
+ ".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='Space']):not([data-key='Tab ↹'])"
785
+ );
786
+ const ThaiAlphabet = '_ๅ/-ภถุึคตจขชๆไำพะัีรนยบลฃฟหกดเ้่าสวงผปแอิืทมใฝ%+๑๒๓๔ู฿๕๖๗๘๙๐"ฎฑธํ๊ณฯญฐ,ฅฤฆฏโฌ็๋ษศซ.ฦฬฒ?์ฺฮฉ)('.split("");
787
+ this.shuffleArray(ThaiAlphabet);
788
+ keys.forEach((key, index) => {
789
+ key.textContent = ThaiAlphabet[index];
790
+ key.dataset.key = ThaiAlphabet[index];
791
+ });
792
+ }
793
+
794
+ // []
795
+ scrambleSymbols() {
796
+ const keys = document.querySelectorAll(
797
+ ":not([data-key='std']):not([data-key='scr']).key:not([data-key=backspace]"
798
+ );
799
+ const numbers = "@#$%^&*()_+~`{}|\\:'<>?/[]±§¶!€£¥¢©®™℅‰†".split("");
800
+ this.shuffleArray(numbers);
801
+ keys.forEach((key, index) => {
802
+ key.textContent = numbers[index];
803
+ key.dataset.key = numbers[index];
804
+ });
805
+ }
806
+
807
+ // [14] จัดการการลากคีย์บอร์ด
808
+ startDrag(event) {
809
+ this.isDragging = true;
810
+ this.offsetX = event.clientX - document.getElementById("keyboard").offsetLeft;
811
+ this.offsetY = event.clientY - document.getElementById("keyboard").offsetTop;
812
+
813
+ document.addEventListener("mousemove", this.drag.bind(this));
814
+ document.addEventListener("mouseup", () => {
815
+ this.isDragging = false;
816
+ document.removeEventListener("mousemove", this.drag.bind(this));
817
+ });
818
+ }
819
+
820
+ // [15]
821
+ drag(event) {
822
+ if (this.isDragging) {
823
+ const keyboard = document.getElementById("keyboard");
824
+ keyboard.style.left = `${event.clientX - this.offsetX}px`;
825
+ keyboard.style.top = `${event.clientY - this.offsetY}px`;
826
+ }
827
+ }
625
828
  }