js_lis 2.0.5 → 2.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/VirtualKeyboard.js +157 -263
- package/layouts.js +4 -4
- package/main.js +1 -1
- package/package.json +1 -1
- package/style/kbstyle.js +10 -0
package/VirtualKeyboard.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { layouts } from "./layouts.js";
|
|
2
|
+
import { injectCSS } from "./style/kbstyle.js";
|
|
2
3
|
|
|
3
4
|
export class VirtualKeyboard {
|
|
4
5
|
constructor() {
|
|
@@ -13,34 +14,16 @@ export class VirtualKeyboard {
|
|
|
13
14
|
this.shiftActive = false;
|
|
14
15
|
this.capsLockActive = false;
|
|
15
16
|
this.isScrambled = false;
|
|
16
|
-
this.isVirtualKeyboardActive = false;
|
|
17
|
+
this.isVirtualKeyboardActive = false;
|
|
17
18
|
|
|
18
|
-
// Maintain plaintext buffers per input (not stored in DOM). DOM will store only encrypted text.
|
|
19
19
|
this.inputPlaintextBuffers = new WeakMap();
|
|
20
|
-
|
|
21
|
-
// Security state
|
|
22
|
-
this.security = {
|
|
23
|
-
detectorStatus: "idle",
|
|
24
|
-
suspiciousEvents: [],
|
|
25
|
-
observer: null,
|
|
26
|
-
addSuspicious(event) {
|
|
27
|
-
try {
|
|
28
|
-
this.suspiciousEvents.push({
|
|
29
|
-
time: new Date().toISOString(),
|
|
30
|
-
...event,
|
|
31
|
-
});
|
|
32
|
-
console.warn("[VK Security] Suspicious activity detected:", event);
|
|
33
|
-
} catch (_) {}
|
|
34
|
-
},
|
|
35
|
-
};
|
|
36
|
-
|
|
20
|
+
|
|
37
21
|
this.initialize();
|
|
38
22
|
}
|
|
39
23
|
|
|
40
24
|
async initialize() {
|
|
41
25
|
try {
|
|
42
|
-
|
|
43
|
-
this.startKeyloggerDetector();
|
|
26
|
+
injectCSS();
|
|
44
27
|
this.render();
|
|
45
28
|
this.initializeInputListeners();
|
|
46
29
|
console.log("VirtualKeyboard initialized successfully.");
|
|
@@ -49,113 +32,6 @@ export class VirtualKeyboard {
|
|
|
49
32
|
}
|
|
50
33
|
}
|
|
51
34
|
|
|
52
|
-
// Inject CSP Report-Only meta for runtime visibility into potential violations
|
|
53
|
-
applyCSPReportOnly() {
|
|
54
|
-
// Disabled: Report-Only CSP must be set via HTTP headers. See server middleware in app.js.
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// Start MutationObserver and limited listener-hook to detect potential keylogger injections
|
|
59
|
-
startKeyloggerDetector() {
|
|
60
|
-
try {
|
|
61
|
-
const allowlistedScriptHosts = new Set([
|
|
62
|
-
window.location.host,
|
|
63
|
-
"cdnjs.cloudflare.com",
|
|
64
|
-
]);
|
|
65
|
-
|
|
66
|
-
const isSuspiciousScript = (node) => {
|
|
67
|
-
try {
|
|
68
|
-
if (!(node instanceof HTMLScriptElement)) return false;
|
|
69
|
-
const src = node.getAttribute("src") || "";
|
|
70
|
-
if (!src) {
|
|
71
|
-
const code = (node.textContent || "").toLowerCase();
|
|
72
|
-
return /addEventListener\(['\"]key|document\.onkey|keylogger|send\(|fetch\(|xmlhttprequest/i.test(code);
|
|
73
|
-
}
|
|
74
|
-
const url = new URL(src, window.location.href);
|
|
75
|
-
return !Array.from(allowlistedScriptHosts).some((h) => url.host.endsWith(h));
|
|
76
|
-
} catch (_) {
|
|
77
|
-
return true;
|
|
78
|
-
}
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
const isSuspiciousElement = (node) => {
|
|
82
|
-
try {
|
|
83
|
-
if (!(node instanceof Element)) return false;
|
|
84
|
-
const hasKeyHandlers = node.hasAttribute("onkeydown") || node.hasAttribute("onkeypress") || node.hasAttribute("onkeyup");
|
|
85
|
-
const hiddenFrame = node.tagName === "IFRAME" && (node.getAttribute("sandbox") === null || node.getAttribute("srcdoc") !== null);
|
|
86
|
-
return hasKeyHandlers || hiddenFrame;
|
|
87
|
-
} catch (_) {
|
|
88
|
-
return false;
|
|
89
|
-
}
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
const quarantineScript = (script) => {
|
|
93
|
-
try {
|
|
94
|
-
script.type = "text/plain";
|
|
95
|
-
script.setAttribute("data-quarantined", "true");
|
|
96
|
-
} catch (_) {}
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
const observer = new MutationObserver((mutations) => {
|
|
100
|
-
for (const m of mutations) {
|
|
101
|
-
if (m.type === "childList") {
|
|
102
|
-
m.addedNodes.forEach((node) => {
|
|
103
|
-
if (isSuspiciousScript(node)) {
|
|
104
|
-
this.security.addSuspicious({ type: "script", reason: "suspicious source or inline pattern", node });
|
|
105
|
-
quarantineScript(node);
|
|
106
|
-
} else if (isSuspiciousElement(node)) {
|
|
107
|
-
this.security.addSuspicious({ type: "element", reason: "inline key handlers or risky frame", node });
|
|
108
|
-
}
|
|
109
|
-
});
|
|
110
|
-
} else if (m.type === "attributes" && isSuspiciousElement(m.target)) {
|
|
111
|
-
this.security.addSuspicious({ type: "attr", reason: `attribute ${m.attributeName}`, node: m.target });
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
observer.observe(document.documentElement, {
|
|
117
|
-
childList: true,
|
|
118
|
-
subtree: true,
|
|
119
|
-
attributes: true,
|
|
120
|
-
attributeFilter: ["onkeydown", "onkeypress", "onkeyup", "src", "type", "sandbox", "srcdoc"],
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
// Hook addEventListener to monitor keyboard listeners registration
|
|
124
|
-
const OriginalAddEventListener = EventTarget.prototype.addEventListener;
|
|
125
|
-
const selfRef = this;
|
|
126
|
-
EventTarget.prototype.addEventListener = function(type, listener, options) {
|
|
127
|
-
try {
|
|
128
|
-
if (type && /^(key(?:down|up|press))$/i.test(type)) {
|
|
129
|
-
const info = {
|
|
130
|
-
type: "event-listener",
|
|
131
|
-
reason: `keyboard listener registered: ${type}`,
|
|
132
|
-
target: this,
|
|
133
|
-
};
|
|
134
|
-
selfRef.security.addSuspicious(info);
|
|
135
|
-
}
|
|
136
|
-
} catch (_) {}
|
|
137
|
-
return OriginalAddEventListener.call(this, type, listener, options);
|
|
138
|
-
};
|
|
139
|
-
|
|
140
|
-
this.security.observer = observer;
|
|
141
|
-
this.security.detectorStatus = "running";
|
|
142
|
-
|
|
143
|
-
// Expose minimal security API
|
|
144
|
-
window.VKSecurity = {
|
|
145
|
-
getStatus: () => this.security.detectorStatus,
|
|
146
|
-
getEvents: () => [...this.security.suspiciousEvents],
|
|
147
|
-
stop: () => {
|
|
148
|
-
try { observer.disconnect(); } catch (_) {}
|
|
149
|
-
this.security.detectorStatus = "stopped";
|
|
150
|
-
},
|
|
151
|
-
};
|
|
152
|
-
console.info("[VK Security] Keylogger detector started");
|
|
153
|
-
} catch (err) {
|
|
154
|
-
this.security.detectorStatus = "error";
|
|
155
|
-
console.warn("[VK Security] Detector error:", err);
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
35
|
getLayoutName(layout) {
|
|
160
36
|
switch (layout) {
|
|
161
37
|
case "full":
|
|
@@ -168,6 +44,8 @@ export class VirtualKeyboard {
|
|
|
168
44
|
return "Numpad Keyboard";
|
|
169
45
|
case "symbols":
|
|
170
46
|
return "Symbols Keyboard";
|
|
47
|
+
case "full-layouts":
|
|
48
|
+
return "Full English + Numpad";
|
|
171
49
|
default:
|
|
172
50
|
return "Unknown Layout";
|
|
173
51
|
}
|
|
@@ -177,40 +55,42 @@ export class VirtualKeyboard {
|
|
|
177
55
|
document.addEventListener("click", (e) => {
|
|
178
56
|
const target = e.target;
|
|
179
57
|
if (target.tagName === "INPUT" || target.tagName === "TEXTAREA") {
|
|
180
|
-
|
|
58
|
+
const clickCursorPos = target.selectionStart;
|
|
59
|
+
this.setCurrentInput(target, !this.isVirtualKeyboardActive);
|
|
60
|
+
setTimeout(() => {
|
|
61
|
+
if (target.selectionStart !== clickCursorPos) {
|
|
62
|
+
target.setSelectionRange(clickCursorPos, clickCursorPos);
|
|
63
|
+
}
|
|
64
|
+
}, 10);
|
|
181
65
|
}
|
|
182
66
|
});
|
|
183
67
|
|
|
184
|
-
document.addEventListener(
|
|
185
|
-
"focus",
|
|
186
|
-
(e) => {
|
|
68
|
+
document.addEventListener("focus", (e) => {
|
|
187
69
|
const target = e.target;
|
|
188
70
|
if (target.tagName === "INPUT" || target.tagName === "TEXTAREA") {
|
|
189
|
-
this.setCurrentInput(target);
|
|
71
|
+
this.setCurrentInput(target, !this.isVirtualKeyboardActive);
|
|
190
72
|
}
|
|
191
73
|
},
|
|
192
74
|
true
|
|
193
75
|
);
|
|
194
76
|
|
|
195
|
-
//
|
|
77
|
+
//Add real keyboard input listeners to handle encryption
|
|
196
78
|
document.addEventListener("input", (e) => {
|
|
197
79
|
const target = e.target;
|
|
198
80
|
if ((target.tagName === "INPUT" || target.tagName === "TEXTAREA") && target === this.currentInput) {
|
|
199
|
-
// Only handle real keyboard input if it's not coming from virtual keyboard operations
|
|
200
81
|
if (!this.isVirtualKeyboardActive) {
|
|
201
82
|
this.handleRealKeyboardInput(target);
|
|
202
83
|
}
|
|
203
84
|
}
|
|
204
85
|
});
|
|
205
86
|
|
|
87
|
+
// Add real keyboard arrow key support
|
|
206
88
|
document.addEventListener("keydown", (e) => {
|
|
207
89
|
const target = e.target;
|
|
208
90
|
if ((target.tagName === "INPUT" || target.tagName === "TEXTAREA") && target === this.currentInput) {
|
|
209
|
-
|
|
210
|
-
if ((e.key === "Backspace" || e.key === "Delete") && !this.isVirtualKeyboardActive) {
|
|
211
|
-
// Wait for the input event to handle the updated value
|
|
91
|
+
if (['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'End'].includes(e.code)) {
|
|
212
92
|
setTimeout(() => {
|
|
213
|
-
|
|
93
|
+
target.focus();
|
|
214
94
|
}, 0);
|
|
215
95
|
}
|
|
216
96
|
}
|
|
@@ -222,7 +102,7 @@ export class VirtualKeyboard {
|
|
|
222
102
|
}
|
|
223
103
|
}
|
|
224
104
|
|
|
225
|
-
setCurrentInput(inputElement) {
|
|
105
|
+
setCurrentInput(inputElement, moveToEnd = true) {
|
|
226
106
|
if (this.currentInput) {
|
|
227
107
|
this.currentInput.classList.remove("keyboard-active");
|
|
228
108
|
}
|
|
@@ -230,6 +110,8 @@ export class VirtualKeyboard {
|
|
|
230
110
|
this.currentInput = inputElement;
|
|
231
111
|
this.currentInput.classList.add("keyboard-active");
|
|
232
112
|
|
|
113
|
+
this.currentInput.focus();
|
|
114
|
+
|
|
233
115
|
// Ensure buffer initialized for this input
|
|
234
116
|
if (!this.inputPlaintextBuffers.has(this.currentInput)) {
|
|
235
117
|
const ds = this.currentInput.dataset ? this.currentInput.dataset.encrypted : undefined;
|
|
@@ -248,6 +130,14 @@ export class VirtualKeyboard {
|
|
|
248
130
|
}
|
|
249
131
|
// Sync visible with buffer (masking for password)
|
|
250
132
|
this.updateDisplayedValue();
|
|
133
|
+
|
|
134
|
+
// Only move cursor to end on initial focus, not during virtual keyboard operations
|
|
135
|
+
if (moveToEnd) {
|
|
136
|
+
setTimeout(() => {
|
|
137
|
+
const length = this.currentInput.value.length;
|
|
138
|
+
this.currentInput.setSelectionRange(length, length);
|
|
139
|
+
}, 0);
|
|
140
|
+
}
|
|
251
141
|
}
|
|
252
142
|
|
|
253
143
|
// [1]
|
|
@@ -407,124 +297,79 @@ export class VirtualKeyboard {
|
|
|
407
297
|
key.classList.toggle("active", this.isScrambled);
|
|
408
298
|
});
|
|
409
299
|
}
|
|
300
|
+
|
|
301
|
+
if (this.currentLayout === "full") {
|
|
302
|
+
if (this.isScrambled) {
|
|
303
|
+
this.unscrambleKeys();
|
|
304
|
+
} else {
|
|
305
|
+
this.scrambleFullLayoutKeys();
|
|
306
|
+
}
|
|
307
|
+
this.isScrambled = !this.isScrambled;
|
|
308
|
+
document
|
|
309
|
+
.querySelectorAll('.key[data-key="scrambled"]')
|
|
310
|
+
.forEach((key) => {
|
|
311
|
+
key.classList.toggle("active", this.isScrambled);
|
|
312
|
+
});
|
|
313
|
+
}
|
|
410
314
|
this.isVirtualKeyboardActive = false;
|
|
411
315
|
return;
|
|
412
316
|
}
|
|
413
317
|
|
|
414
318
|
switch (keyPressed) {
|
|
415
|
-
case "Esc":
|
|
416
|
-
const modals = document.querySelectorAll(".modal");
|
|
417
|
-
if (modals.length === 0) {
|
|
418
|
-
document.exitFullscreen();
|
|
419
|
-
console.warn("No modals found to close.");
|
|
420
|
-
}
|
|
421
|
-
modals.forEach((modal) => {
|
|
422
|
-
modal.classList.add("hidden");
|
|
423
|
-
});
|
|
424
|
-
break;
|
|
425
|
-
|
|
426
|
-
case "F1":
|
|
427
|
-
break;
|
|
428
|
-
|
|
429
|
-
case "F2":
|
|
430
|
-
// เปิดโหมดแก้ไขสำหรับ element ที่เลือก
|
|
431
|
-
const activeElement = document.activeElement;
|
|
432
|
-
if (activeElement && activeElement.contentEditable !== undefined) {
|
|
433
|
-
activeElement.contentEditable = true;
|
|
434
|
-
activeElement.focus();
|
|
435
|
-
console.log(activeElement.contentEditable);
|
|
436
|
-
} else {
|
|
437
|
-
console.warn("No editable element found.");
|
|
438
|
-
}
|
|
439
|
-
break;
|
|
440
|
-
|
|
441
|
-
case "F3":
|
|
442
|
-
// เปิดการค้นหา
|
|
443
|
-
event.preventDefault();
|
|
444
|
-
this.option.openSearch();
|
|
445
|
-
break;
|
|
446
|
-
|
|
447
|
-
case "F4":
|
|
448
|
-
// เปิดเมนูการตั้งค่า
|
|
449
|
-
event.preventDefault();
|
|
450
|
-
this.option.openSettings();
|
|
451
|
-
break;
|
|
452
|
-
|
|
453
|
-
case "F5":
|
|
454
|
-
// รีโหลดหน้าเว็บ (คงเดิม)
|
|
455
|
-
window.location.reload();
|
|
456
|
-
break;
|
|
457
|
-
|
|
458
|
-
case "F6":
|
|
459
|
-
// สลับระหว่างโหมดกลางวัน/กลางคืน
|
|
460
|
-
document.body.classList.toggle("dark-mode");
|
|
461
|
-
break;
|
|
462
|
-
|
|
463
|
-
case "F7":
|
|
464
|
-
break;
|
|
465
|
-
|
|
466
|
-
case "F8":
|
|
467
|
-
break;
|
|
468
|
-
|
|
469
|
-
case "F9":
|
|
470
|
-
break;
|
|
471
|
-
|
|
472
|
-
case "F10":
|
|
473
|
-
break;
|
|
474
|
-
|
|
475
|
-
case "F11":
|
|
476
|
-
if (!document.fullscreenElement) {
|
|
477
|
-
document.documentElement
|
|
478
|
-
.requestFullscreen()
|
|
479
|
-
.catch((err) =>
|
|
480
|
-
console.error("Error attempting to enable fullscreen:", err)
|
|
481
|
-
);
|
|
482
|
-
} else {
|
|
483
|
-
document
|
|
484
|
-
.exitFullscreen()
|
|
485
|
-
.catch((err) =>
|
|
486
|
-
console.error("Error attempting to exit fullscreen:", err)
|
|
487
|
-
);
|
|
488
|
-
}
|
|
489
|
-
break;
|
|
490
|
-
|
|
491
|
-
case "F12":
|
|
492
|
-
break;
|
|
493
|
-
|
|
494
319
|
case "HOME":
|
|
495
|
-
this.currentInput.setSelectionRange(
|
|
496
|
-
|
|
320
|
+
this.currentInput.setSelectionRange(0, 0);
|
|
321
|
+
this.currentInput.focus();
|
|
322
|
+
setTimeout(() => {
|
|
323
|
+
this.currentInput.setSelectionRange(0, 0);
|
|
324
|
+
this.currentInput.focus();
|
|
325
|
+
}, 5);
|
|
326
|
+
// Skip the general cursor repositioning for navigation keys
|
|
327
|
+
this.isVirtualKeyboardActive = false;
|
|
328
|
+
return;
|
|
497
329
|
|
|
498
330
|
case "END":
|
|
499
|
-
const
|
|
500
|
-
this.currentInput.setSelectionRange(
|
|
501
|
-
|
|
331
|
+
const bufferLengthEnd = this.getCurrentBuffer().length;
|
|
332
|
+
this.currentInput.setSelectionRange(bufferLengthEnd, bufferLengthEnd);
|
|
333
|
+
this.currentInput.focus();
|
|
334
|
+
setTimeout(() => {
|
|
335
|
+
this.currentInput.setSelectionRange(bufferLengthEnd, bufferLengthEnd);
|
|
336
|
+
this.currentInput.focus();
|
|
337
|
+
}, 5);
|
|
338
|
+
// Skip the general cursor repositioning for navigation keys
|
|
339
|
+
this.isVirtualKeyboardActive = false;
|
|
340
|
+
return;
|
|
502
341
|
|
|
503
342
|
case "Backspace":
|
|
504
343
|
case "backspace":
|
|
505
344
|
if (start === end && start > 0) {
|
|
506
345
|
const newBuffer = buffer.slice(0, start - 1) + buffer.slice(end);
|
|
507
346
|
this.setCurrentBuffer(newBuffer);
|
|
508
|
-
|
|
347
|
+
const newCursorPos = start - 1;
|
|
348
|
+
this.updateDisplayedValue(false);
|
|
349
|
+
this.currentInput.setSelectionRange(newCursorPos, newCursorPos);
|
|
509
350
|
} else {
|
|
510
351
|
const newBuffer = buffer.slice(0, start) + buffer.slice(end);
|
|
511
352
|
this.setCurrentBuffer(newBuffer);
|
|
512
|
-
|
|
353
|
+
const newCursorPos = start;
|
|
354
|
+
this.updateDisplayedValue(false);
|
|
355
|
+
this.currentInput.setSelectionRange(newCursorPos, newCursorPos);
|
|
513
356
|
}
|
|
514
|
-
this.updateDisplayedValue();
|
|
515
357
|
break;
|
|
516
358
|
|
|
517
359
|
case "DEL⌦":
|
|
518
360
|
if (start === end && start < buffer.length) {
|
|
519
361
|
const newBuffer = buffer.slice(0, start) + buffer.slice(end + 1);
|
|
520
362
|
this.setCurrentBuffer(newBuffer);
|
|
521
|
-
|
|
363
|
+
const newCursorPos = start;
|
|
364
|
+
this.updateDisplayedValue(false);
|
|
365
|
+
this.currentInput.setSelectionRange(newCursorPos, newCursorPos);
|
|
522
366
|
} else {
|
|
523
367
|
const newBuffer = buffer.slice(0, start) + buffer.slice(end);
|
|
524
368
|
this.setCurrentBuffer(newBuffer);
|
|
525
|
-
|
|
369
|
+
const newCursorPos = start;
|
|
370
|
+
this.updateDisplayedValue(false);
|
|
371
|
+
this.currentInput.setSelectionRange(newCursorPos, newCursorPos);
|
|
526
372
|
}
|
|
527
|
-
this.updateDisplayedValue();
|
|
528
373
|
break;
|
|
529
374
|
|
|
530
375
|
case "Space":
|
|
@@ -539,8 +384,9 @@ export class VirtualKeyboard {
|
|
|
539
384
|
if (this.currentInput.tagName === "TEXTAREA") {
|
|
540
385
|
const newBuffer = buffer.slice(0, start) + "\n" + buffer.slice(end);
|
|
541
386
|
this.setCurrentBuffer(newBuffer);
|
|
542
|
-
|
|
543
|
-
this.
|
|
387
|
+
const newCursorPos = start + 1;
|
|
388
|
+
this.updateDisplayedValue(false);
|
|
389
|
+
this.currentInput.setSelectionRange(newCursorPos, newCursorPos);
|
|
544
390
|
} else if (this.currentInput.tagName === "INPUT" || this.currentInput.type === "password" || this.currentInput.type === "text" ) {
|
|
545
391
|
if (this.currentInput.form) {
|
|
546
392
|
const submitButton = this.currentInput.form.querySelector(
|
|
@@ -550,7 +396,7 @@ export class VirtualKeyboard {
|
|
|
550
396
|
} else {
|
|
551
397
|
const newBuffer = buffer + "\n";
|
|
552
398
|
this.setCurrentBuffer(newBuffer);
|
|
553
|
-
this.updateDisplayedValue();
|
|
399
|
+
this.updateDisplayedValue(false);
|
|
554
400
|
}
|
|
555
401
|
}
|
|
556
402
|
break;
|
|
@@ -564,33 +410,50 @@ export class VirtualKeyboard {
|
|
|
564
410
|
break;
|
|
565
411
|
|
|
566
412
|
case "←":
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
Math.max(0, start - 1)
|
|
570
|
-
);
|
|
413
|
+
const leftNewPos = Math.max(0, start - 1);
|
|
414
|
+
this.currentInput.setSelectionRange(leftNewPos, leftNewPos);
|
|
571
415
|
break;
|
|
572
416
|
|
|
573
417
|
case "→":
|
|
574
|
-
this.
|
|
418
|
+
const bufferLength = this.getCurrentBuffer().length;
|
|
419
|
+
const rightNewPos = Math.min(bufferLength, start + 1);
|
|
420
|
+
this.currentInput.setSelectionRange(rightNewPos, rightNewPos);
|
|
575
421
|
break;
|
|
576
422
|
|
|
577
423
|
case "↑":
|
|
578
424
|
case "↓":
|
|
579
|
-
|
|
580
|
-
const
|
|
581
|
-
const
|
|
582
|
-
const
|
|
583
|
-
const
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
425
|
+
// Use the actual buffer content instead of displayed value
|
|
426
|
+
const actualText = this.getCurrentBuffer();
|
|
427
|
+
const actualLines = actualText.substring(0, start).split("\n");
|
|
428
|
+
const actualCurrentLineIndex = actualLines.length - 1;
|
|
429
|
+
const actualCurrentLine = actualLines[actualCurrentLineIndex];
|
|
430
|
+
const actualColumnIndex = start - actualText.lastIndexOf("\n", start - 1) - 1;
|
|
431
|
+
|
|
432
|
+
if (keyPressed === "↑" && actualCurrentLineIndex > 0) {
|
|
433
|
+
// Move to previous line
|
|
434
|
+
const prevLineLength = actualLines[actualCurrentLineIndex - 1].length;
|
|
435
|
+
const upNewPos = start - actualCurrentLine.length - 1 - Math.max(0, actualColumnIndex - prevLineLength);
|
|
436
|
+
this.currentInput.setSelectionRange(Math.max(0, upNewPos), Math.max(0, upNewPos));
|
|
437
|
+
} else if (keyPressed === "↓") {
|
|
438
|
+
// Move to next line
|
|
439
|
+
const remainingText = actualText.substring(start);
|
|
440
|
+
const nextLineBreak = remainingText.indexOf("\n");
|
|
441
|
+
|
|
442
|
+
if (nextLineBreak !== -1) {
|
|
443
|
+
// There is a next line
|
|
444
|
+
const textAfterNextLineBreak = remainingText.substring(nextLineBreak + 1);
|
|
445
|
+
const nextLineEnd = textAfterNextLineBreak.indexOf("\n");
|
|
446
|
+
const nextLineLength = nextLineEnd === -1 ? textAfterNextLineBreak.length : nextLineEnd;
|
|
447
|
+
|
|
448
|
+
const downNewPos = start + nextLineBreak + 1 + Math.min(actualColumnIndex, nextLineLength);
|
|
449
|
+
this.currentInput.setSelectionRange(downNewPos, downNewPos);
|
|
450
|
+
}
|
|
451
|
+
// If no next line, do nothing (cursor stays at current position)
|
|
593
452
|
}
|
|
453
|
+
|
|
454
|
+
this.currentInput.focus();
|
|
455
|
+
// Skip the general cursor repositioning for arrow keys
|
|
456
|
+
this.isVirtualKeyboardActive = false;
|
|
594
457
|
break;
|
|
595
458
|
|
|
596
459
|
default:
|
|
@@ -600,12 +463,17 @@ export class VirtualKeyboard {
|
|
|
600
463
|
|
|
601
464
|
if (isShiftActive && !isCapsActive) this.toggleShift();
|
|
602
465
|
|
|
466
|
+
this.currentInput.focus();
|
|
467
|
+
|
|
468
|
+
setTimeout(() => {
|
|
469
|
+
this.currentInput.focus();
|
|
470
|
+
const cursorPos = this.currentInput.selectionStart;
|
|
471
|
+
this.currentInput.setSelectionRange(cursorPos, cursorPos);
|
|
472
|
+
}, 0);
|
|
473
|
+
|
|
603
474
|
this.currentInput.focus();
|
|
604
475
|
const event = new Event("input", { bubbles: true });
|
|
605
476
|
this.currentInput.dispatchEvent(event);
|
|
606
|
-
|
|
607
|
-
// Reset flag after virtual keyboard operation is complete
|
|
608
|
-
this.isVirtualKeyboardActive = false;
|
|
609
477
|
}
|
|
610
478
|
|
|
611
479
|
// [3]
|
|
@@ -617,8 +485,12 @@ export class VirtualKeyboard {
|
|
|
617
485
|
const buffer = this.getCurrentBuffer();
|
|
618
486
|
const newBuffer = buffer.slice(0, start) + textvalue + buffer.slice(end);
|
|
619
487
|
this.setCurrentBuffer(newBuffer);
|
|
620
|
-
|
|
621
|
-
|
|
488
|
+
|
|
489
|
+
const newCursorPos = start + textvalue.length;
|
|
490
|
+
|
|
491
|
+
this.updateDisplayedValue(false);
|
|
492
|
+
|
|
493
|
+
this.currentInput.setSelectionRange(newCursorPos, newCursorPos);
|
|
622
494
|
}
|
|
623
495
|
|
|
624
496
|
// Helper: get current plaintext buffer for active input
|
|
@@ -635,12 +507,22 @@ export class VirtualKeyboard {
|
|
|
635
507
|
}
|
|
636
508
|
|
|
637
509
|
// Helper: reflect masked value in the visible input/textarea
|
|
638
|
-
updateDisplayedValue() {
|
|
510
|
+
updateDisplayedValue(preserveCursor = true) {
|
|
639
511
|
if (!this.currentInput) return;
|
|
640
512
|
const buffer = this.getCurrentBuffer();
|
|
641
513
|
const isPassword = this.currentInput.type === "password";
|
|
642
514
|
const maskChar = "•";
|
|
515
|
+
|
|
516
|
+
// Store current cursor position only if we want to preserve it
|
|
517
|
+
const currentStart = preserveCursor ? this.currentInput.selectionStart : 0;
|
|
518
|
+
const currentEnd = preserveCursor ? this.currentInput.selectionEnd : 0;
|
|
519
|
+
|
|
643
520
|
this.currentInput.value = isPassword ? maskChar.repeat(buffer.length) : buffer;
|
|
521
|
+
|
|
522
|
+
// Restore cursor position after updating value only if preserving
|
|
523
|
+
if (preserveCursor) {
|
|
524
|
+
this.currentInput.setSelectionRange(currentStart, currentEnd);
|
|
525
|
+
}
|
|
644
526
|
}
|
|
645
527
|
|
|
646
528
|
// Handle real keyboard input and encrypt it
|
|
@@ -655,9 +537,6 @@ export class VirtualKeyboard {
|
|
|
655
537
|
|
|
656
538
|
// Encrypt and store in data-encrypted attribute
|
|
657
539
|
this.updateDomEncrypted(input, currentValue);
|
|
658
|
-
// this.updateDisplayedValue();
|
|
659
|
-
// console.log("Real keyboard input captured and encrypted for:", input.id || input.name || "unnamed input");
|
|
660
|
-
// console.log("Input type:", input.type, "Value length:", currentValue.length);
|
|
661
540
|
}
|
|
662
541
|
|
|
663
542
|
// Helper: encrypt and store ciphertext in DOM attribute
|
|
@@ -820,7 +699,7 @@ export class VirtualKeyboard {
|
|
|
820
699
|
// [12]
|
|
821
700
|
scrambleEnglishKeys() {
|
|
822
701
|
const keys = document.querySelectorAll(
|
|
823
|
-
".key:not([data-key='scrambled']):not([data-key='std']):not([data-key='scr']):not([data-key='
|
|
702
|
+
".key:not([data-key='scrambled']):not([data-key='std']):not([data-key='scr']):not([data-key=' ']):not([data-key='Backspace']):not([data-key='Caps 🄰']):not([data-key='Shift ⇧']):not([data-key='Enter']):not([data-key='Tab ↹'])"
|
|
824
703
|
);
|
|
825
704
|
const englishAlphabet = "abcdefghijklmnopqrstuvwxyz/.,';\\][`1234567890-=".split("");
|
|
826
705
|
this.shuffleArray(englishAlphabet);
|
|
@@ -833,7 +712,7 @@ export class VirtualKeyboard {
|
|
|
833
712
|
// [13]
|
|
834
713
|
scrambleThaiKeys() {
|
|
835
714
|
const keys = document.querySelectorAll(
|
|
836
|
-
".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='
|
|
715
|
+
".key:not([data-key='scrambled']):not([data-key='scr']):not([data-key='Backspace']):not([data-key='Caps 🄰']):not([data-key='Shift ⇧']):not([data-key='Enter']):not([data-key=' ']):not([data-key='Tab ↹'])"
|
|
837
716
|
);
|
|
838
717
|
const ThaiAlphabet = '_ๅ/-ภถุึคตจขชๆไำพะัีรนยบลฃฟหกดเ้่าสวงผปแอิืทมใฝ%+๑๒๓๔ู฿๕๖๗๘๙๐"ฎฑธํ๊ณฯญฐ,ฅฤฆฏโฌ็๋ษศซ.ฦฬฒ?์ฺฮฉ)('.split("");
|
|
839
718
|
this.shuffleArray(ThaiAlphabet);
|
|
@@ -856,6 +735,21 @@ export class VirtualKeyboard {
|
|
|
856
735
|
});
|
|
857
736
|
}
|
|
858
737
|
|
|
738
|
+
|
|
739
|
+
scrambleFullLayoutKeys() {
|
|
740
|
+
const keys = document.querySelectorAll(
|
|
741
|
+
".key:not(.concat-keys):not([data-key='scrambled']):not([data-key='std']):not([data-key='scr']):not([data-key=' ']):not([data-key='Backspace']):not([data-key='Caps 🄰']):not([data-key='Shift ⇧']):not([data-key='Enter']):not([data-key='Tab ↹'])"
|
|
742
|
+
);
|
|
743
|
+
const englishChars = "abcdefghijklmnopqrstuvwxyz1234567890;'\\/][`,.-=".split("");
|
|
744
|
+
this.shuffleArray(englishChars);
|
|
745
|
+
keys.forEach((key, index) => {
|
|
746
|
+
if (index < englishChars.length) {
|
|
747
|
+
key.textContent = englishChars[index];
|
|
748
|
+
key.dataset.key = englishChars[index];
|
|
749
|
+
}
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
|
|
859
753
|
// [14] จัดการการลากคีย์บอร์ด
|
|
860
754
|
startDrag(event) {
|
|
861
755
|
this.isDragging = true;
|
package/layouts.js
CHANGED
|
@@ -1,25 +1,25 @@
|
|
|
1
1
|
export const layouts = {
|
|
2
2
|
full: [
|
|
3
|
-
["Esc", "F1", "F2", "F3", "F4", "F5", "F6", "F7", "F8", "F9", "F10", "F11", "F12", "DEL⌦", "HOME", "END"],
|
|
3
|
+
// ["Esc", "F1", "F2", "F3", "F4", "F5", "F6", "F7", "F8", "F9", "F10", "F11", "F12", "DEL⌦", "HOME", "END"],"ㅤ","ㅤ","ㅤ","ㅤ",
|
|
4
4
|
["`", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "-", "=", "Backspace"].concat(["+", "-", "*","/"]),
|
|
5
5
|
["Tab ↹", "q", "w", "e", "r", "t", "y", "u", "i", "o", "p", "[", "]", "\\"].concat(["7", "8", "9", "%"]),
|
|
6
6
|
["Caps 🄰", "a", "s", "d", "f", "g", "h", "j", "k", "l", ";", "'", "Enter"].concat(["4", "5", "6", "_"]),
|
|
7
7
|
["Shift ⇧", "z", "x", "c", "v", "b", "n", "m", ",", ".", "/", "Shift ⇧", "↑"].concat(["1", "2", "3", "="]),
|
|
8
|
-
["
|
|
8
|
+
["scrambled", " ", "←", "↓", "→"].concat(["0", "."]),
|
|
9
9
|
],
|
|
10
10
|
en: [
|
|
11
11
|
["`", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "-", "=", "Backspace"],
|
|
12
12
|
["Tab ↹", "q", "w", "e", "r", "t", "y", "u", "i", "o", "p", "[", "]", "\\"],
|
|
13
13
|
["Caps 🄰", "a", "s", "d", "f", "g", "h", "j", "k", "l", ";", "'", "Enter"],
|
|
14
14
|
["Shift ⇧", "z", "x", "c", "v", "b", "n", "m", ",", ".", "/", "Shift ⇧"],
|
|
15
|
-
["scrambled", "
|
|
15
|
+
["scrambled", " "],
|
|
16
16
|
],
|
|
17
17
|
th: [
|
|
18
18
|
["_", "ๅ", "/", "-", "ภ", "ถ", "ุ", "ึ", "ค", "ต", "จ", "ข", "ช", "Backspace"],
|
|
19
19
|
["Tab ↹", "ๆ", "ไ", "ำ", "พ", "ะ", "ั", "ี", "ร", "น", "ย", "บ", "ล", "ฃ"],
|
|
20
20
|
["Caps 🄰", "ฟ", "ห", "ก", "ด", "เ", "้", "่", "า", "ส", "ว", "ง", "Enter"],
|
|
21
21
|
["Shift ⇧", "ผ", "ป", "แ", "อ", "ิ", "ื", "ท", "ม", "ใ", "ฝ", "Shift ⇧"],
|
|
22
|
-
["scrambled" ,"
|
|
22
|
+
["scrambled" ," "],
|
|
23
23
|
],
|
|
24
24
|
numpad: [
|
|
25
25
|
["scr", "+", "-", "*"],
|
package/main.js
CHANGED
package/package.json
CHANGED
package/style/kbstyle.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// CSS-in-JS version of kbstyle.css
|
|
2
|
+
export const css = `
|
|
3
|
+
.keyboard-row,.virtual-keyboard{display:flex;display:flex}.keyb,.keyboard-key{cursor:pointer;font-size:16px;text-align:center}.virtual-keyboard{flex-direction:column;width:100%;max-width:800px;margin:0 auto;border:2px solid #333;border-radius:10px;padding:20px;background-color:#f5f5f5;box-shadow:0 4px 10px rgba(0,0,0,.1)}.keyboard-row{justify-content:space-between}.keyboard-key{flex:1;padding:10px 15px;margin:2px;border:1px solid #ccc;border-radius:5px;background-color:#fff;transition:background-color .2s}.keyboard-active,input:focus,textarea:focus{border-color:#007bff;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.backspacew{flex:1.5;width:260px}.keyboard-key:hover{background:#e0e0e0}.keyboard-key:active{background:#d0d0d0;transform:translateY(1px)}.keyboard-row{display:flex;justify-content:flex-start;margin:5px 0}.controls{margin-bottom:20px}button,select{margin-right:10px;padding:8px 12px;border:1px solid #ccc;border-radius:4px;font-size:14px;cursor:pointer}button:hover{background:#f0f0f0}input:focus,textarea:focus{outline:0}#keyboard{position:absolute;cursor:move;top:80%;left:50%;transform:translate(-50%,-50%)}.keyb{color:#fff;border:#000;width:40px;height:25px;line-height:50px;margin:8px}.keyboard-row .active{background:#c7c7c7}body.dark-mode .virtual-keyboard{background-color:#333;box-shadow:0 4px 8px rgba(247,245,245,.1)}body.dark-mode{background-color:#333}body.dark-mode .login-container{background-color:#000;box-shadow:0 4px 8px rgba(247,245,245,.1);color:#fff}body.dark-mode .login-container input{background-color:#3333335d;color:#fff}body.dark-mode .login-container button{background-color:#005703;color:#fff}.fa-keyboard{color:#000}body.dark-mode .keyb .fa-keyboard{color:#ffffffde}body.dark-mode .keyboard-key.key{background-color:#222;color:#ffffffde}[data-key="Caps 🄰"],[data-key="Shift ⇧"],[data-key="Tab ↹"]{min-width:90px}.keyboard-key.key[data-key="Shift ⇧"]{color:#333}.virtual-keyboard.full [data-key=Enter]{min-width:95px}.virtual-keyboard.full [data-key="Shift ⇧"]{min-width:92px}.virtual-keyboard.full [data-key=" "]{min-width:563.8px}.virtual-keyboard.full .concat-keys[data-key="0"]{min-width:102px}.virtual-keyboard.full{min-width:1000px}.virtual-keyboard.Scnum,.virtual-keyboard.numpad{max-width:300px}.hidden{display:none!important}[data-key="Tab ↹"],[data-key=Backspace]{min-width:80px}[data-key="Shift ⇧"]{min-width:100px}[data-key=" "]{min-width:670px}
|
|
4
|
+
`;
|
|
5
|
+
|
|
6
|
+
export function injectCSS() {
|
|
7
|
+
const style = document.createElement('style');
|
|
8
|
+
style.textContent = css;
|
|
9
|
+
document.head.appendChild(style);
|
|
10
|
+
}
|