js_lis 2.0.4 → 2.0.6
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 +278 -244
- 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() {
|
|
@@ -19,28 +20,29 @@ export class VirtualKeyboard {
|
|
|
19
20
|
this.inputPlaintextBuffers = new WeakMap();
|
|
20
21
|
|
|
21
22
|
// Security state
|
|
22
|
-
this.security = {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
};
|
|
36
|
-
|
|
23
|
+
// this.security = {
|
|
24
|
+
// detectorStatus: "idle",
|
|
25
|
+
// suspiciousEvents: [],
|
|
26
|
+
// observer: null,
|
|
27
|
+
// addSuspicious(event) {
|
|
28
|
+
// try {
|
|
29
|
+
// this.suspiciousEvents.push({
|
|
30
|
+
// time: new Date().toISOString(),
|
|
31
|
+
// ...event,
|
|
32
|
+
// });
|
|
33
|
+
// console.warn("[VK Security] Suspicious activity detected:", event);
|
|
34
|
+
// } catch (_) {}
|
|
35
|
+
// },
|
|
36
|
+
// };
|
|
37
|
+
|
|
38
|
+
injectCSS();
|
|
37
39
|
this.initialize();
|
|
38
40
|
}
|
|
39
41
|
|
|
40
42
|
async initialize() {
|
|
41
43
|
try {
|
|
42
|
-
this.applyCSPReportOnly();
|
|
43
|
-
this.startKeyloggerDetector();
|
|
44
|
+
// this.applyCSPReportOnly();
|
|
45
|
+
// this.startKeyloggerDetector();
|
|
44
46
|
this.render();
|
|
45
47
|
this.initializeInputListeners();
|
|
46
48
|
console.log("VirtualKeyboard initialized successfully.");
|
|
@@ -50,111 +52,111 @@ export class VirtualKeyboard {
|
|
|
50
52
|
}
|
|
51
53
|
|
|
52
54
|
// Inject CSP Report-Only meta for runtime visibility into potential violations
|
|
53
|
-
applyCSPReportOnly() {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
55
|
+
// applyCSPReportOnly() {
|
|
56
|
+
// // Disabled: Report-Only CSP must be set via HTTP headers. See server middleware in app.js.
|
|
57
|
+
// return;
|
|
58
|
+
// }
|
|
57
59
|
|
|
58
60
|
// Start MutationObserver and limited listener-hook to detect potential keylogger injections
|
|
59
|
-
startKeyloggerDetector() {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
}
|
|
61
|
+
// startKeyloggerDetector() {
|
|
62
|
+
// try {
|
|
63
|
+
// const allowlistedScriptHosts = new Set([
|
|
64
|
+
// window.location.host,
|
|
65
|
+
// "cdnjs.cloudflare.com",
|
|
66
|
+
// ]);
|
|
67
|
+
|
|
68
|
+
// const isSuspiciousScript = (node) => {
|
|
69
|
+
// try {
|
|
70
|
+
// if (!(node instanceof HTMLScriptElement)) return false;
|
|
71
|
+
// const src = node.getAttribute("src") || "";
|
|
72
|
+
// if (!src) {
|
|
73
|
+
// const code = (node.textContent || "").toLowerCase();
|
|
74
|
+
// return /addEventListener\(['\"]key|document\.onkey|keylogger|send\(|fetch\(|xmlhttprequest/i.test(code);
|
|
75
|
+
// }
|
|
76
|
+
// const url = new URL(src, window.location.href);
|
|
77
|
+
// return !Array.from(allowlistedScriptHosts).some((h) => url.host.endsWith(h));
|
|
78
|
+
// } catch (_) {
|
|
79
|
+
// return true;
|
|
80
|
+
// }
|
|
81
|
+
// };
|
|
82
|
+
|
|
83
|
+
// const isSuspiciousElement = (node) => {
|
|
84
|
+
// try {
|
|
85
|
+
// if (!(node instanceof Element)) return false;
|
|
86
|
+
// const hasKeyHandlers = node.hasAttribute("onkeydown") || node.hasAttribute("onkeypress") || node.hasAttribute("onkeyup");
|
|
87
|
+
// const hiddenFrame = node.tagName === "IFRAME" && (node.getAttribute("sandbox") === null || node.getAttribute("srcdoc") !== null);
|
|
88
|
+
// return hasKeyHandlers || hiddenFrame;
|
|
89
|
+
// } catch (_) {
|
|
90
|
+
// return false;
|
|
91
|
+
// }
|
|
92
|
+
// };
|
|
93
|
+
|
|
94
|
+
// const quarantineScript = (script) => {
|
|
95
|
+
// try {
|
|
96
|
+
// script.type = "text/plain";
|
|
97
|
+
// script.setAttribute("data-quarantined", "true");
|
|
98
|
+
// } catch (_) {}
|
|
99
|
+
// };
|
|
100
|
+
|
|
101
|
+
// const observer = new MutationObserver((mutations) => {
|
|
102
|
+
// for (const m of mutations) {
|
|
103
|
+
// if (m.type === "childList") {
|
|
104
|
+
// m.addedNodes.forEach((node) => {
|
|
105
|
+
// if (isSuspiciousScript(node)) {
|
|
106
|
+
// this.security.addSuspicious({ type: "script", reason: "suspicious source or inline pattern", node });
|
|
107
|
+
// quarantineScript(node);
|
|
108
|
+
// } else if (isSuspiciousElement(node)) {
|
|
109
|
+
// this.security.addSuspicious({ type: "element", reason: "inline key handlers or risky frame", node });
|
|
110
|
+
// }
|
|
111
|
+
// });
|
|
112
|
+
// } else if (m.type === "attributes" && isSuspiciousElement(m.target)) {
|
|
113
|
+
// this.security.addSuspicious({ type: "attr", reason: `attribute ${m.attributeName}`, node: m.target });
|
|
114
|
+
// }
|
|
115
|
+
// }
|
|
116
|
+
// });
|
|
117
|
+
|
|
118
|
+
// observer.observe(document.documentElement, {
|
|
119
|
+
// childList: true,
|
|
120
|
+
// subtree: true,
|
|
121
|
+
// attributes: true,
|
|
122
|
+
// attributeFilter: ["onkeydown", "onkeypress", "onkeyup", "src", "type", "sandbox", "srcdoc"],
|
|
123
|
+
// });
|
|
124
|
+
|
|
125
|
+
// // Hook addEventListener to monitor keyboard listeners registration
|
|
126
|
+
// const OriginalAddEventListener = EventTarget.prototype.addEventListener;
|
|
127
|
+
// const selfRef = this;
|
|
128
|
+
// EventTarget.prototype.addEventListener = function(type, listener, options) {
|
|
129
|
+
// try {
|
|
130
|
+
// if (type && /^(key(?:down|up|press))$/i.test(type)) {
|
|
131
|
+
// const info = {
|
|
132
|
+
// type: "event-listener",
|
|
133
|
+
// reason: `keyboard listener registered: ${type}`,
|
|
134
|
+
// target: this,
|
|
135
|
+
// };
|
|
136
|
+
// selfRef.security.addSuspicious(info);
|
|
137
|
+
// }
|
|
138
|
+
// } catch (_) {}
|
|
139
|
+
// return OriginalAddEventListener.call(this, type, listener, options);
|
|
140
|
+
// };
|
|
141
|
+
|
|
142
|
+
// this.security.observer = observer;
|
|
143
|
+
// this.security.detectorStatus = "running";
|
|
144
|
+
|
|
145
|
+
// // Expose minimal security API
|
|
146
|
+
// window.VKSecurity = {
|
|
147
|
+
// getStatus: () => this.security.detectorStatus,
|
|
148
|
+
// getEvents: () => [...this.security.suspiciousEvents],
|
|
149
|
+
// stop: () => {
|
|
150
|
+
// try { observer.disconnect(); } catch (_) {}
|
|
151
|
+
// this.security.detectorStatus = "stopped";
|
|
152
|
+
// },
|
|
153
|
+
// };
|
|
154
|
+
// console.info("[VK Security] Keylogger detector started");
|
|
155
|
+
// } catch (err) {
|
|
156
|
+
// this.security.detectorStatus = "error";
|
|
157
|
+
// console.warn("[VK Security] Detector error:", err);
|
|
158
|
+
// }
|
|
159
|
+
// }
|
|
158
160
|
|
|
159
161
|
getLayoutName(layout) {
|
|
160
162
|
switch (layout) {
|
|
@@ -177,7 +179,18 @@ export class VirtualKeyboard {
|
|
|
177
179
|
document.addEventListener("click", (e) => {
|
|
178
180
|
const target = e.target;
|
|
179
181
|
if (target.tagName === "INPUT" || target.tagName === "TEXTAREA") {
|
|
180
|
-
|
|
182
|
+
// Store the cursor position immediately after click
|
|
183
|
+
const clickCursorPos = target.selectionStart;
|
|
184
|
+
|
|
185
|
+
// Don't move cursor to end if virtual keyboard is currently active
|
|
186
|
+
this.setCurrentInput(target, !this.isVirtualKeyboardActive);
|
|
187
|
+
|
|
188
|
+
// Restore the cursor position that user clicked to set
|
|
189
|
+
setTimeout(() => {
|
|
190
|
+
if (target.selectionStart !== clickCursorPos) {
|
|
191
|
+
target.setSelectionRange(clickCursorPos, clickCursorPos);
|
|
192
|
+
}
|
|
193
|
+
}, 10);
|
|
181
194
|
}
|
|
182
195
|
});
|
|
183
196
|
|
|
@@ -186,13 +199,14 @@ export class VirtualKeyboard {
|
|
|
186
199
|
(e) => {
|
|
187
200
|
const target = e.target;
|
|
188
201
|
if (target.tagName === "INPUT" || target.tagName === "TEXTAREA") {
|
|
189
|
-
|
|
202
|
+
// Don't move cursor to end if virtual keyboard is currently active
|
|
203
|
+
this.setCurrentInput(target, !this.isVirtualKeyboardActive);
|
|
190
204
|
}
|
|
191
205
|
},
|
|
192
206
|
true
|
|
193
207
|
);
|
|
194
208
|
|
|
195
|
-
//
|
|
209
|
+
//Add real keyboard input listeners to handle encryption
|
|
196
210
|
document.addEventListener("input", (e) => {
|
|
197
211
|
const target = e.target;
|
|
198
212
|
if ((target.tagName === "INPUT" || target.tagName === "TEXTAREA") && target === this.currentInput) {
|
|
@@ -203,14 +217,16 @@ export class VirtualKeyboard {
|
|
|
203
217
|
}
|
|
204
218
|
});
|
|
205
219
|
|
|
220
|
+
// Add real keyboard arrow key support
|
|
206
221
|
document.addEventListener("keydown", (e) => {
|
|
207
222
|
const target = e.target;
|
|
208
223
|
if ((target.tagName === "INPUT" || target.tagName === "TEXTAREA") && target === this.currentInput) {
|
|
209
|
-
// Handle
|
|
210
|
-
if (
|
|
211
|
-
//
|
|
224
|
+
// Handle arrow keys, HOME, END from real keyboard
|
|
225
|
+
if (['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'End'].includes(e.code)) {
|
|
226
|
+
// Let the browser handle the navigation naturally
|
|
227
|
+
// Just ensure the input stays focused
|
|
212
228
|
setTimeout(() => {
|
|
213
|
-
|
|
229
|
+
target.focus();
|
|
214
230
|
}, 0);
|
|
215
231
|
}
|
|
216
232
|
}
|
|
@@ -222,7 +238,7 @@ export class VirtualKeyboard {
|
|
|
222
238
|
}
|
|
223
239
|
}
|
|
224
240
|
|
|
225
|
-
setCurrentInput(inputElement) {
|
|
241
|
+
setCurrentInput(inputElement, moveToEnd = true) {
|
|
226
242
|
if (this.currentInput) {
|
|
227
243
|
this.currentInput.classList.remove("keyboard-active");
|
|
228
244
|
}
|
|
@@ -230,6 +246,9 @@ export class VirtualKeyboard {
|
|
|
230
246
|
this.currentInput = inputElement;
|
|
231
247
|
this.currentInput.classList.add("keyboard-active");
|
|
232
248
|
|
|
249
|
+
// Ensure the input is focused and cursor is visible
|
|
250
|
+
this.currentInput.focus();
|
|
251
|
+
|
|
233
252
|
// Ensure buffer initialized for this input
|
|
234
253
|
if (!this.inputPlaintextBuffers.has(this.currentInput)) {
|
|
235
254
|
const ds = this.currentInput.dataset ? this.currentInput.dataset.encrypted : undefined;
|
|
@@ -248,6 +267,14 @@ export class VirtualKeyboard {
|
|
|
248
267
|
}
|
|
249
268
|
// Sync visible with buffer (masking for password)
|
|
250
269
|
this.updateDisplayedValue();
|
|
270
|
+
|
|
271
|
+
// Only move cursor to end on initial focus, not during virtual keyboard operations
|
|
272
|
+
if (moveToEnd) {
|
|
273
|
+
setTimeout(() => {
|
|
274
|
+
const length = this.currentInput.value.length;
|
|
275
|
+
this.currentInput.setSelectionRange(length, length);
|
|
276
|
+
}, 0);
|
|
277
|
+
}
|
|
251
278
|
}
|
|
252
279
|
|
|
253
280
|
// [1]
|
|
@@ -412,119 +439,60 @@ export class VirtualKeyboard {
|
|
|
412
439
|
}
|
|
413
440
|
|
|
414
441
|
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
442
|
case "HOME":
|
|
495
|
-
this.currentInput.setSelectionRange(
|
|
496
|
-
|
|
443
|
+
this.currentInput.setSelectionRange(0, 0);
|
|
444
|
+
this.currentInput.focus();
|
|
445
|
+
setTimeout(() => {
|
|
446
|
+
this.currentInput.setSelectionRange(0, 0);
|
|
447
|
+
this.currentInput.focus();
|
|
448
|
+
}, 5);
|
|
449
|
+
// Skip the general cursor repositioning for navigation keys
|
|
450
|
+
this.isVirtualKeyboardActive = false;
|
|
451
|
+
return;
|
|
497
452
|
|
|
498
453
|
case "END":
|
|
499
|
-
const
|
|
500
|
-
this.currentInput.setSelectionRange(
|
|
501
|
-
|
|
454
|
+
const bufferLengthEnd = this.getCurrentBuffer().length;
|
|
455
|
+
this.currentInput.setSelectionRange(bufferLengthEnd, bufferLengthEnd);
|
|
456
|
+
this.currentInput.focus();
|
|
457
|
+
setTimeout(() => {
|
|
458
|
+
this.currentInput.setSelectionRange(bufferLengthEnd, bufferLengthEnd);
|
|
459
|
+
this.currentInput.focus();
|
|
460
|
+
}, 5);
|
|
461
|
+
// Skip the general cursor repositioning for navigation keys
|
|
462
|
+
this.isVirtualKeyboardActive = false;
|
|
463
|
+
return;
|
|
502
464
|
|
|
503
465
|
case "Backspace":
|
|
504
466
|
case "backspace":
|
|
505
467
|
if (start === end && start > 0) {
|
|
506
468
|
const newBuffer = buffer.slice(0, start - 1) + buffer.slice(end);
|
|
507
469
|
this.setCurrentBuffer(newBuffer);
|
|
508
|
-
|
|
470
|
+
const newCursorPos = start - 1;
|
|
471
|
+
this.updateDisplayedValue(false);
|
|
472
|
+
this.currentInput.setSelectionRange(newCursorPos, newCursorPos);
|
|
509
473
|
} else {
|
|
510
474
|
const newBuffer = buffer.slice(0, start) + buffer.slice(end);
|
|
511
475
|
this.setCurrentBuffer(newBuffer);
|
|
512
|
-
|
|
476
|
+
const newCursorPos = start;
|
|
477
|
+
this.updateDisplayedValue(false);
|
|
478
|
+
this.currentInput.setSelectionRange(newCursorPos, newCursorPos);
|
|
513
479
|
}
|
|
514
|
-
this.updateDisplayedValue();
|
|
515
480
|
break;
|
|
516
481
|
|
|
517
482
|
case "DEL⌦":
|
|
518
483
|
if (start === end && start < buffer.length) {
|
|
519
484
|
const newBuffer = buffer.slice(0, start) + buffer.slice(end + 1);
|
|
520
485
|
this.setCurrentBuffer(newBuffer);
|
|
521
|
-
|
|
486
|
+
const newCursorPos = start;
|
|
487
|
+
this.updateDisplayedValue(false);
|
|
488
|
+
this.currentInput.setSelectionRange(newCursorPos, newCursorPos);
|
|
522
489
|
} else {
|
|
523
490
|
const newBuffer = buffer.slice(0, start) + buffer.slice(end);
|
|
524
491
|
this.setCurrentBuffer(newBuffer);
|
|
525
|
-
|
|
492
|
+
const newCursorPos = start;
|
|
493
|
+
this.updateDisplayedValue(false);
|
|
494
|
+
this.currentInput.setSelectionRange(newCursorPos, newCursorPos);
|
|
526
495
|
}
|
|
527
|
-
this.updateDisplayedValue();
|
|
528
496
|
break;
|
|
529
497
|
|
|
530
498
|
case "Space":
|
|
@@ -539,8 +507,9 @@ export class VirtualKeyboard {
|
|
|
539
507
|
if (this.currentInput.tagName === "TEXTAREA") {
|
|
540
508
|
const newBuffer = buffer.slice(0, start) + "\n" + buffer.slice(end);
|
|
541
509
|
this.setCurrentBuffer(newBuffer);
|
|
542
|
-
|
|
543
|
-
this.
|
|
510
|
+
const newCursorPos = start + 1;
|
|
511
|
+
this.updateDisplayedValue(false);
|
|
512
|
+
this.currentInput.setSelectionRange(newCursorPos, newCursorPos);
|
|
544
513
|
} else if (this.currentInput.tagName === "INPUT" || this.currentInput.type === "password" || this.currentInput.type === "text" ) {
|
|
545
514
|
if (this.currentInput.form) {
|
|
546
515
|
const submitButton = this.currentInput.form.querySelector(
|
|
@@ -550,7 +519,7 @@ export class VirtualKeyboard {
|
|
|
550
519
|
} else {
|
|
551
520
|
const newBuffer = buffer + "\n";
|
|
552
521
|
this.setCurrentBuffer(newBuffer);
|
|
553
|
-
this.updateDisplayedValue();
|
|
522
|
+
this.updateDisplayedValue(false);
|
|
554
523
|
}
|
|
555
524
|
}
|
|
556
525
|
break;
|
|
@@ -564,33 +533,66 @@ export class VirtualKeyboard {
|
|
|
564
533
|
break;
|
|
565
534
|
|
|
566
535
|
case "←":
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
);
|
|
536
|
+
const leftNewPos = Math.max(0, start - 1);
|
|
537
|
+
this.currentInput.setSelectionRange(leftNewPos, leftNewPos);
|
|
538
|
+
// Force immediate focus and ensure cursor is visible
|
|
539
|
+
this.currentInput.focus();
|
|
540
|
+
setTimeout(() => {
|
|
541
|
+
this.currentInput.setSelectionRange(leftNewPos, leftNewPos);
|
|
542
|
+
this.currentInput.focus();
|
|
543
|
+
}, 5);
|
|
544
|
+
// Skip the general cursor repositioning for arrow keys
|
|
545
|
+
this.isVirtualKeyboardActive = false;
|
|
571
546
|
break;
|
|
572
547
|
|
|
573
548
|
case "→":
|
|
574
|
-
this.
|
|
549
|
+
const bufferLength = this.getCurrentBuffer().length;
|
|
550
|
+
const rightNewPos = Math.min(bufferLength, start + 1);
|
|
551
|
+
this.currentInput.setSelectionRange(rightNewPos, rightNewPos);
|
|
552
|
+
// Force immediate focus and ensure cursor is visible
|
|
553
|
+
this.currentInput.focus();
|
|
554
|
+
setTimeout(() => {
|
|
555
|
+
this.currentInput.setSelectionRange(rightNewPos, rightNewPos);
|
|
556
|
+
this.currentInput.focus();
|
|
557
|
+
}, 5);
|
|
558
|
+
// Skip the general cursor repositioning for arrow keys
|
|
559
|
+
this.isVirtualKeyboardActive = false;
|
|
575
560
|
break;
|
|
576
561
|
|
|
577
562
|
case "↑":
|
|
578
563
|
case "↓":
|
|
579
|
-
|
|
580
|
-
const
|
|
581
|
-
const
|
|
582
|
-
const
|
|
583
|
-
const
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
564
|
+
// Use the actual buffer content instead of displayed value
|
|
565
|
+
const actualText = this.getCurrentBuffer();
|
|
566
|
+
const actualLines = actualText.substring(0, start).split("\n");
|
|
567
|
+
const actualCurrentLineIndex = actualLines.length - 1;
|
|
568
|
+
const actualCurrentLine = actualLines[actualCurrentLineIndex];
|
|
569
|
+
const actualColumnIndex = start - actualText.lastIndexOf("\n", start - 1) - 1;
|
|
570
|
+
|
|
571
|
+
if (keyPressed === "↑" && actualCurrentLineIndex > 0) {
|
|
572
|
+
// Move to previous line
|
|
573
|
+
const prevLineLength = actualLines[actualCurrentLineIndex - 1].length;
|
|
574
|
+
const upNewPos = start - actualCurrentLine.length - 1 - Math.max(0, actualColumnIndex - prevLineLength);
|
|
575
|
+
this.currentInput.setSelectionRange(Math.max(0, upNewPos), Math.max(0, upNewPos));
|
|
576
|
+
} else if (keyPressed === "↓") {
|
|
577
|
+
// Move to next line
|
|
578
|
+
const remainingText = actualText.substring(start);
|
|
579
|
+
const nextLineBreak = remainingText.indexOf("\n");
|
|
580
|
+
|
|
581
|
+
if (nextLineBreak !== -1) {
|
|
582
|
+
// There is a next line
|
|
583
|
+
const textAfterNextLineBreak = remainingText.substring(nextLineBreak + 1);
|
|
584
|
+
const nextLineEnd = textAfterNextLineBreak.indexOf("\n");
|
|
585
|
+
const nextLineLength = nextLineEnd === -1 ? textAfterNextLineBreak.length : nextLineEnd;
|
|
586
|
+
|
|
587
|
+
const downNewPos = start + nextLineBreak + 1 + Math.min(actualColumnIndex, nextLineLength);
|
|
588
|
+
this.currentInput.setSelectionRange(downNewPos, downNewPos);
|
|
589
|
+
}
|
|
590
|
+
// If no next line, do nothing (cursor stays at current position)
|
|
593
591
|
}
|
|
592
|
+
|
|
593
|
+
this.currentInput.focus();
|
|
594
|
+
// Skip the general cursor repositioning for arrow keys
|
|
595
|
+
this.isVirtualKeyboardActive = false;
|
|
594
596
|
break;
|
|
595
597
|
|
|
596
598
|
default:
|
|
@@ -600,12 +602,24 @@ export class VirtualKeyboard {
|
|
|
600
602
|
|
|
601
603
|
if (isShiftActive && !isCapsActive) this.toggleShift();
|
|
602
604
|
|
|
605
|
+
// Only apply general focus and cursor management if not an arrow key
|
|
606
|
+
// Arrow keys handle their own cursor positioning and return early
|
|
603
607
|
this.currentInput.focus();
|
|
608
|
+
|
|
609
|
+
// Use setTimeout to ensure cursor positioning happens after DOM updates
|
|
610
|
+
setTimeout(() => {
|
|
611
|
+
this.currentInput.focus();
|
|
612
|
+
// Ensure cursor is visible by setting selection range
|
|
613
|
+
const cursorPos = this.currentInput.selectionStart;
|
|
614
|
+
this.currentInput.setSelectionRange(cursorPos, cursorPos);
|
|
615
|
+
}, 0);
|
|
616
|
+
|
|
604
617
|
const event = new Event("input", { bubbles: true });
|
|
605
618
|
this.currentInput.dispatchEvent(event);
|
|
606
619
|
|
|
607
620
|
// Reset flag after virtual keyboard operation is complete
|
|
608
621
|
this.isVirtualKeyboardActive = false;
|
|
622
|
+
// console.log(keyPressed)
|
|
609
623
|
}
|
|
610
624
|
|
|
611
625
|
// [3]
|
|
@@ -617,8 +631,18 @@ export class VirtualKeyboard {
|
|
|
617
631
|
const buffer = this.getCurrentBuffer();
|
|
618
632
|
const newBuffer = buffer.slice(0, start) + textvalue + buffer.slice(end);
|
|
619
633
|
this.setCurrentBuffer(newBuffer);
|
|
620
|
-
|
|
621
|
-
|
|
634
|
+
|
|
635
|
+
// Calculate new cursor position first
|
|
636
|
+
const newCursorPos = start + textvalue.length;
|
|
637
|
+
|
|
638
|
+
// Update displayed value without preserving old cursor position
|
|
639
|
+
this.updateDisplayedValue(false);
|
|
640
|
+
|
|
641
|
+
// Set cursor position after the inserted text
|
|
642
|
+
this.currentInput.setSelectionRange(newCursorPos, newCursorPos);
|
|
643
|
+
|
|
644
|
+
// Ensure the input remains focused with visible cursor
|
|
645
|
+
// this.currentInput.focus();
|
|
622
646
|
}
|
|
623
647
|
|
|
624
648
|
// Helper: get current plaintext buffer for active input
|
|
@@ -635,12 +659,22 @@ export class VirtualKeyboard {
|
|
|
635
659
|
}
|
|
636
660
|
|
|
637
661
|
// Helper: reflect masked value in the visible input/textarea
|
|
638
|
-
updateDisplayedValue() {
|
|
662
|
+
updateDisplayedValue(preserveCursor = true) {
|
|
639
663
|
if (!this.currentInput) return;
|
|
640
664
|
const buffer = this.getCurrentBuffer();
|
|
641
665
|
const isPassword = this.currentInput.type === "password";
|
|
642
666
|
const maskChar = "•";
|
|
667
|
+
|
|
668
|
+
// Store current cursor position only if we want to preserve it
|
|
669
|
+
const currentStart = preserveCursor ? this.currentInput.selectionStart : 0;
|
|
670
|
+
const currentEnd = preserveCursor ? this.currentInput.selectionEnd : 0;
|
|
671
|
+
|
|
643
672
|
this.currentInput.value = isPassword ? maskChar.repeat(buffer.length) : buffer;
|
|
673
|
+
|
|
674
|
+
// Restore cursor position after updating value only if preserving
|
|
675
|
+
if (preserveCursor) {
|
|
676
|
+
this.currentInput.setSelectionRange(currentStart, currentEnd);
|
|
677
|
+
}
|
|
644
678
|
}
|
|
645
679
|
|
|
646
680
|
// Handle real keyboard input and encrypt it
|
|
@@ -656,6 +690,7 @@ export class VirtualKeyboard {
|
|
|
656
690
|
// Encrypt and store in data-encrypted attribute
|
|
657
691
|
this.updateDomEncrypted(input, currentValue);
|
|
658
692
|
|
|
693
|
+
// this.updateDisplayedValue();
|
|
659
694
|
// console.log("Real keyboard input captured and encrypted for:", input.id || input.name || "unnamed input");
|
|
660
695
|
// console.log("Input type:", input.type, "Value length:", currentValue.length);
|
|
661
696
|
}
|
|
@@ -679,7 +714,6 @@ export class VirtualKeyboard {
|
|
|
679
714
|
input.dataset.encrypted = "";
|
|
680
715
|
}
|
|
681
716
|
}
|
|
682
|
-
|
|
683
717
|
|
|
684
718
|
// [4]
|
|
685
719
|
unscrambleKeys() {
|
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
|
+
["@", " ", "←", "↓", "→"].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:620.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
|
+
}
|