js_lis 2.0.5 → 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.
@@ -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
- 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
-
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
- // Disabled: Report-Only CSP must be set via HTTP headers. See server middleware in app.js.
55
- return;
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
- 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
- }
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
- this.setCurrentInput(target);
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
- this.setCurrentInput(target);
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
- // Add real keyboard input listeners to handle encryption
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 special keys that might not trigger input event
210
- if ((e.key === "Backspace" || e.key === "Delete") && !this.isVirtualKeyboardActive) {
211
- // Wait for the input event to handle the updated value
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
- this.handleRealKeyboardInput(target);
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(Math.max(0, 0), Math.max(0, 0));
496
- break;
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 length = this.currentInput.value.length;
500
- this.currentInput.setSelectionRange(length, length);
501
- break;
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
- this.currentInput.selectionStart = this.currentInput.selectionEnd = start - 1;
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
- this.currentInput.selectionStart = this.currentInput.selectionEnd = start;
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
- this.currentInput.selectionStart = this.currentInput.selectionEnd = start;
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
- this.currentInput.selectionStart = this.currentInput.selectionEnd = start;
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
- this.updateDisplayedValue();
543
- this.currentInput.setSelectionRange(start + 1, start + 1);
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
- this.currentInput.setSelectionRange(
568
- Math.max(0, start - 1),
569
- Math.max(0, start - 1)
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.currentInput.setSelectionRange(start + 1, start + 1);
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
- const text = this.currentInput.value;
580
- const lines = text.substring(0, start).split("\n");
581
- const currentLineIndex = lines.length - 1;
582
- const currentLine = lines[currentLineIndex];
583
- const columnIndex = start - text.lastIndexOf("\n", start - 1) - 1;
584
-
585
- if (keyPressed === "↑" && currentLineIndex > 0) {
586
- const prevLineLength = lines[currentLineIndex - 1].length;
587
- const newPos = start - currentLine.length - 1 - Math.min(columnIndex, prevLineLength);
588
- this.currentInput.setSelectionRange(newPos, newPos);
589
- } else if (keyPressed === "↓" && currentLineIndex < lines.length - 1) {
590
- const nextLine = lines[currentLineIndex + 1];
591
- const newPos = start + currentLine.length + 1 + Math.min(columnIndex, nextLine.length);
592
- this.currentInput.setSelectionRange(newPos, newPos);
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
- this.updateDisplayedValue();
621
- this.currentInput.selectionStart = this.currentInput.selectionEnd = start + textvalue.length;
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
@@ -655,6 +689,7 @@ export class VirtualKeyboard {
655
689
 
656
690
  // Encrypt and store in data-encrypted attribute
657
691
  this.updateDomEncrypted(input, currentValue);
692
+
658
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);
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
- ["Space", "←", "↓", "→"].concat(["0", "."]),
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", "Space"],
15
+ ["scrambled", " "],
16
16
  ],
17
17
  th: [
18
18
  ["_", "ๅ", "/", "-", "ภ", "ถ", "ุ", "ึ", "ค", "ต", "จ", "ข", "ช", "Backspace"],
19
19
  ["Tab ↹", "ๆ", "ไ", "ำ", "พ", "ะ", "ั", "ี", "ร", "น", "ย", "บ", "ล", "ฃ"],
20
20
  ["Caps 🄰", "ฟ", "ห", "ก", "ด", "เ", "้", "่", "า", "ส", "ว", "ง", "Enter"],
21
21
  ["Shift ⇧", "ผ", "ป", "แ", "อ", "ิ", "ื", "ท", "ม", "ใ", "ฝ", "Shift ⇧"],
22
- ["scrambled" ,"Space"],
22
+ ["scrambled" ," "],
23
23
  ],
24
24
  numpad: [
25
25
  ["scr", "+", "-", "*"],
package/main.js CHANGED
@@ -34,7 +34,7 @@ window.onload = () => {
34
34
  try {
35
35
  // Initialize per-page crypto helper
36
36
  window.VKCrypto = createSessionCrypto();
37
-
37
+
38
38
  window.keyboard = new VirtualKeyboard();
39
39
  console.log("VirtualKeyboard initialized successfully.");
40
40
  } catch (error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "js_lis",
3
- "version": "2.0.5",
3
+ "version": "2.0.6",
4
4
  "main": "index.js",
5
5
  "scripts": {
6
6
  "test": "echo \"Error: no test specified\" && exit 1"
@@ -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
+ }